concurrency

This commit is contained in:
John Burwell 2026-05-25 23:26:58 -05:00
parent 08574cb259
commit 56f7ea7055
9 changed files with 1742 additions and 149 deletions

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
DerivedData/
# macOS
.DS_Store
# Xcode user data
*.xcuserstate
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
# Swift Package Manager local state
.swiftpm/

View File

@ -36,6 +36,7 @@ struct ContentView: View {
revealFooterURLAction: revealURLInFinder(_:), revealFooterURLAction: revealURLInFinder(_:),
filters: sidebarFilters(for:) filters: sidebarFilters(for:)
) )
.navigationSplitViewColumnWidth(min: 280, ideal: 320, max: 380)
} content: { } content: {
ItemListColumnView( ItemListColumnView(
isEmpty: library.sources.isEmpty, isEmpty: library.sources.isEmpty,
@ -52,11 +53,15 @@ struct ContentView: View {
refreshAction: rescanCurrentSource, refreshAction: rescanCurrentSource,
itemContextMenu: itemContextMenu(for:) itemContextMenu: itemContextMenu(for:)
) )
.navigationSplitViewColumnWidth(min: 340, ideal: 400, max: 460)
} detail: { } detail: {
ItemDetailColumnView( ItemDetailColumnView(
item: currentSelectedItem, item: currentSelectedItem,
behaviorPacks: currentSelectedItem.map { packReferences(for: $0, type: .behaviorPack) } ?? [], behaviorPacks: currentSelectedItem.map { logicalPackReferences(for: $0, type: .behaviorPack) } ?? [],
resourcePacks: currentSelectedItem.map { packReferences(for: $0, type: .resourcePack) } ?? [], resourcePacks: currentSelectedItem.map { logicalPackReferences(for: $0, type: .resourcePack) } ?? [],
worldsUsingPack: currentSelectedItem.map(worldsUsingPack(for:)) ?? [],
backingPackInstances: currentSelectedItem.map(backingPackInstances(for:)) ?? [],
isSuspiciousPack: currentSelectedItem.map(isSuspiciousPack(_:)) ?? false,
contents: currentSelectedItem.map(directoryPreviewEntries(for:)) ?? [], contents: currentSelectedItem.map(directoryPreviewEntries(for:)) ?? [],
directoryPreviewLimit: directoryPreviewLimit, directoryPreviewLimit: directoryPreviewLimit,
isEmpty: library.sources.isEmpty, isEmpty: library.sources.isEmpty,
@ -84,6 +89,7 @@ struct ContentView: View {
shareItem(item, from: anchorView) shareItem(item, from: anchorView)
} }
) )
.frame(minWidth: 450)
} }
.onChange(of: displayedItems.map(\.id)) { _, filteredIDs in .onChange(of: displayedItems.map(\.id)) { _, filteredIDs in
guard let selectedItemID, !filteredIDs.contains(selectedItemID) else { guard let selectedItemID, !filteredIDs.contains(selectedItemID) else {
@ -368,8 +374,54 @@ struct ContentView: View {
} }
} }
private func packReferences(for item: MinecraftContentItem, type: MinecraftContentType) -> [ContentPackReference] { private func logicalPackReferences(for item: MinecraftContentItem, type: MinecraftContentType) -> [ContentPackReference] {
item.packReferences.filter { $0.type == type } guard
item.contentType == .world,
let source = currentSource
else {
return []
}
return source.resolvedPackReferences(for: item.id, type: type)
}
private func worldsUsingPack(for item: MinecraftContentItem) -> [MinecraftContentItem] {
guard
(item.contentType == .behaviorPack || item.contentType == .resourcePack),
let source = currentSource,
let logicalPack = source.logicalPack(forRepresentativeItemID: item.id)
else {
return []
}
return source.worldsUsingPack(logicalPack.id).sorted(by: sortComparator)
}
private func backingPackInstances(for item: MinecraftContentItem) -> [MinecraftContentItem] {
guard
(item.contentType == .behaviorPack || item.contentType == .resourcePack),
let source = currentSource,
let logicalPack = source.logicalPack(forRepresentativeItemID: item.id)
else {
return []
}
return source
.packInstances(for: logicalPack.id)
.compactMap { source.rawItem(withID: $0.itemID) }
.sorted(by: WorldScanner.sortItems)
}
private func isSuspiciousPack(_ item: MinecraftContentItem) -> Bool {
guard
(item.contentType == .behaviorPack || item.contentType == .resourcePack),
let source = currentSource,
let logicalPack = source.logicalPack(forRepresentativeItemID: item.id)
else {
return false
}
return logicalPack.isSuspicious
} }
private func directoryPreviewEntries(for item: MinecraftContentItem) -> [DirectoryPreviewEntry] { private func directoryPreviewEntries(for item: MinecraftContentItem) -> [DirectoryPreviewEntry] {
@ -716,7 +768,7 @@ private struct SidebarFilterRow: View {
private struct SidebarSourcesSectionHeaderView: View { private struct SidebarSourcesSectionHeaderView: View {
var body: some View { var body: some View {
Text("Library") Text("Libraries")
.font(.headline) .font(.headline)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.textCase(nil) .textCase(nil)
@ -849,6 +901,9 @@ private struct ItemDetailColumnView: View {
let item: MinecraftContentItem? let item: MinecraftContentItem?
let behaviorPacks: [ContentPackReference] let behaviorPacks: [ContentPackReference]
let resourcePacks: [ContentPackReference] let resourcePacks: [ContentPackReference]
let worldsUsingPack: [MinecraftContentItem]
let backingPackInstances: [MinecraftContentItem]
let isSuspiciousPack: Bool
let contents: [DirectoryPreviewEntry] let contents: [DirectoryPreviewEntry]
let directoryPreviewLimit: Int let directoryPreviewLimit: Int
let isEmpty: Bool let isEmpty: Bool
@ -868,6 +923,9 @@ private struct ItemDetailColumnView: View {
item: item, item: item,
behaviorPacks: behaviorPacks, behaviorPacks: behaviorPacks,
resourcePacks: resourcePacks, resourcePacks: resourcePacks,
worldsUsingPack: worldsUsingPack,
backingPackInstances: backingPackInstances,
isSuspiciousPack: isSuspiciousPack,
contents: contents, contents: contents,
directoryPreviewLimit: directoryPreviewLimit directoryPreviewLimit: directoryPreviewLimit
) )
@ -924,7 +982,7 @@ private struct ContentRowView: View {
Spacer() Spacer()
if !item.metadataLoaded { if !item.metadataLoaded || !item.sizeLoaded {
ProgressView() ProgressView()
.controlSize(.small) .controlSize(.small)
} }
@ -934,9 +992,14 @@ private struct ContentRowView: View {
} }
private var metadataLine: String { private var metadataLine: String {
let sizeText = item.sizeBytes.map { let sizeText: String
ByteCountFormatter.string(fromByteCount: $0, countStyle: .file) if let sizeBytes = item.sizeBytes {
} ?? "Size unavailable" sizeText = ByteCountFormatter.string(fromByteCount: sizeBytes, countStyle: .file)
} else if item.metadataLoaded {
sizeText = "Calculating size..."
} else {
sizeText = "Loading metadata..."
}
let dateText = item.displayDate.map { let dateText = item.displayDate.map {
$0.formatted(date: .abbreviated, time: .omitted) $0.formatted(date: .abbreviated, time: .omitted)
} ?? "Date unavailable" } ?? "Date unavailable"
@ -949,6 +1012,9 @@ private struct ItemDetailView: View {
let item: MinecraftContentItem let item: MinecraftContentItem
let behaviorPacks: [ContentPackReference] let behaviorPacks: [ContentPackReference]
let resourcePacks: [ContentPackReference] let resourcePacks: [ContentPackReference]
let worldsUsingPack: [MinecraftContentItem]
let backingPackInstances: [MinecraftContentItem]
let isSuspiciousPack: Bool
let contents: [DirectoryPreviewEntry] let contents: [DirectoryPreviewEntry]
let directoryPreviewLimit: Int let directoryPreviewLimit: Int
@State private var isTechnicalDetailsExpanded = false @State private var isTechnicalDetailsExpanded = false
@ -975,6 +1041,12 @@ private struct ItemDetailView: View {
Text("Details") Text("Details")
.font(.headline) .font(.headline)
if isSuspiciousPack {
Label("Manifest UUID is missing or unreadable for this pack.", systemImage: "exclamationmark.triangle")
.font(.subheadline)
.foregroundStyle(.orange)
}
detailValueRow(title: "Size", value: sizeText) detailValueRow(title: "Size", value: sizeText)
detailValueRow(title: item.displayDateLabel, value: displayDateText) detailValueRow(title: item.displayDateLabel, value: displayDateText)
@ -1004,6 +1076,52 @@ private struct ItemDetailView: View {
} }
} }
if (item.contentType == .behaviorPack || item.contentType == .resourcePack), !worldsUsingPack.isEmpty {
detailCard {
VStack(alignment: .leading, spacing: 14) {
Text("Used By Worlds")
.font(.headline)
ForEach(worldsUsingPack) { world in
HStack(alignment: .top, spacing: 12) {
PackReferenceIconView(iconURL: world.iconURL)
VStack(alignment: .leading, spacing: 2) {
Text(world.displayName)
Text(worldUsageSecondaryText(for: world))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
}
}
if (item.contentType == .behaviorPack || item.contentType == .resourcePack), !backingPackInstances.isEmpty {
detailCard {
VStack(alignment: .leading, spacing: 14) {
Text("Pack Instances")
.font(.headline)
ForEach(backingPackInstances) { instance in
HStack(alignment: .top, spacing: 12) {
PackReferenceIconView(iconURL: instance.iconURL)
VStack(alignment: .leading, spacing: 2) {
Text(instance.folderName)
Text(packInstanceSecondaryText(for: instance))
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
}
}
detailCard { detailCard {
DisclosureGroup(isExpanded: $isTechnicalDetailsExpanded) { DisclosureGroup(isExpanded: $isTechnicalDetailsExpanded) {
VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 18) {
@ -1052,7 +1170,7 @@ private struct ItemDetailView: View {
} }
} }
.padding(28) .padding(28)
.frame(maxWidth: 760, alignment: .leading) .frame(maxWidth: 450, alignment: .leading)
} }
} }
@ -1113,7 +1231,11 @@ private struct ItemDetailView: View {
} }
private var sizeText: String { private var sizeText: String {
item.sizeBytes.map { ByteCountFormatter.string(fromByteCount: $0, countStyle: .file) } ?? "Unknown" if let sizeBytes = item.sizeBytes {
return ByteCountFormatter.string(fromByteCount: sizeBytes, countStyle: .file)
}
return item.metadataLoaded ? "Calculating..." : "Loading..."
} }
private var displayDateText: String { private var displayDateText: String {
@ -1125,6 +1247,19 @@ private struct ItemDetailView: View {
.compactMap { $0 } .compactMap { $0 }
return components.isEmpty ? nil : components.joined(separator: "") return components.isEmpty ? nil : components.joined(separator: "")
} }
private func worldUsageSecondaryText(for world: MinecraftContentItem) -> String {
let dateText = world.displayDate?.formatted(date: .abbreviated, time: .omitted) ?? "Date unavailable"
return "\(world.displayDateLabel) \(dateText)"
}
private func packInstanceSecondaryText(for instance: MinecraftContentItem) -> String {
if instance.folderURL.pathComponents.contains(MinecraftContentType.world.collectionFolderName) {
return "Embedded in world copy"
}
return "Top-level pack folder"
}
} }
private struct DirectoryPreviewEntry: Identifiable { private struct DirectoryPreviewEntry: Identifiable {

View File

@ -0,0 +1,150 @@
//
// LibraryIndex.swift
// World Manager for Minecraft
//
// Created by OpenAI on 2026-05-25.
//
import Foundation
enum PackIdentitySource: String, Hashable, Sendable {
case manifestUUID
case fallback
}
struct PackIdentity: Hashable, Sendable, Identifiable {
let type: MinecraftContentType
let uuid: String?
let version: String?
let fallbackName: String
let fallbackLocationHint: String?
let source: PackIdentitySource
var id: String {
[
type.rawValue,
uuid ?? normalizedFallbackName,
version ?? "",
fallbackLocationHint ?? ""
].joined(separator: "::")
}
var isSuspicious: Bool {
source == .fallback
}
init(
type: MinecraftContentType,
uuid: String?,
version: String?,
fallbackName: String,
fallbackLocationHint: String?
) {
self.type = type
self.uuid = uuid?.lowercased()
self.version = version
self.fallbackName = fallbackName.trimmingCharacters(in: .whitespacesAndNewlines)
self.fallbackLocationHint = fallbackLocationHint
self.source = self.uuid == nil ? .fallback : .manifestUUID
}
private var normalizedFallbackName: String {
fallbackName.lowercased()
}
static func == (lhs: PackIdentity, rhs: PackIdentity) -> Bool {
guard lhs.type == rhs.type else {
return false
}
if let lhsUUID = lhs.uuid, let rhsUUID = rhs.uuid {
return lhsUUID == rhsUUID && lhs.version == rhs.version
}
return lhs.uuid == rhs.uuid
&& lhs.version == rhs.version
&& lhs.normalizedFallbackName == rhs.normalizedFallbackName
&& lhs.fallbackLocationHint == rhs.fallbackLocationHint
}
func hash(into hasher: inout Hasher) {
hasher.combine(type)
if let uuid {
hasher.combine(uuid)
hasher.combine(version)
return
}
hasher.combine(uuid)
hasher.combine(version)
hasher.combine(normalizedFallbackName)
hasher.combine(fallbackLocationHint)
}
}
struct PackInstance: Identifiable, Hashable, Sendable {
let id: URL
let itemID: URL
let sourceID: URL
let logicalPackID: PackIdentity
let origin: PackSource
let hostWorldItemID: URL?
}
struct LogicalPack: Identifiable, Hashable, Sendable {
let id: PackIdentity
let contentType: MinecraftContentType
let displayName: String
let uuid: String?
let version: String?
let representativeItemID: URL
let instanceItemIDs: [URL]
let isSuspicious: Bool
}
struct LogicalWorld: Identifiable, Hashable, Sendable {
let id: URL
let itemID: URL
let usedPackIDs: [PackIdentity]
let unresolvedReferences: [ContentPackReference]
}
struct WorldPackRelationship: Identifiable, Hashable, Sendable {
let worldItemID: URL
let logicalPackID: PackIdentity?
let reference: ContentPackReference
var id: String {
[
worldItemID.path,
logicalPackID?.id ?? "unresolved",
reference.id
].joined(separator: "::")
}
}
struct ItemSnapshot: Identifiable, Hashable, Sendable {
let id: URL
let relativePath: String
let modifiedDate: Date?
let sizeBytes: Int64?
let packUUID: String?
let packVersion: String?
}
struct CollectionSnapshot: Identifiable, Hashable, Sendable {
let folderName: String
let modifiedDate: Date?
let childDirectoryCount: Int
let fingerprint: String
var id: String { folderName }
}
struct SourceSnapshot: Hashable, Sendable {
let sourceID: URL
let rootModifiedDate: Date?
let collectionSnapshots: [CollectionSnapshot]
let itemSnapshots: [ItemSnapshot]
}

View File

@ -104,8 +104,11 @@ struct MinecraftContentItem: Identifiable, Hashable, Sendable {
var lastPlayedDate: Date? var lastPlayedDate: Date?
var modifiedDate: Date? var modifiedDate: Date?
var sizeBytes: Int64? var sizeBytes: Int64?
var packUUID: String?
var packVersion: String?
var packReferences: [ContentPackReference] var packReferences: [ContentPackReference]
var metadataLoaded: Bool var metadataLoaded: Bool
var sizeLoaded: Bool
nonisolated init( nonisolated init(
folderURL: URL, folderURL: URL,
@ -117,8 +120,11 @@ struct MinecraftContentItem: Identifiable, Hashable, Sendable {
lastPlayedDate: Date? = nil, lastPlayedDate: Date? = nil,
modifiedDate: Date? = nil, modifiedDate: Date? = nil,
sizeBytes: Int64? = nil, sizeBytes: Int64? = nil,
packUUID: String? = nil,
packVersion: String? = nil,
packReferences: [ContentPackReference] = [], packReferences: [ContentPackReference] = [],
metadataLoaded: Bool = false metadataLoaded: Bool = false,
sizeLoaded: Bool = false
) { ) {
self.id = folderURL.standardizedFileURL self.id = folderURL.standardizedFileURL
self.folderURL = folderURL self.folderURL = folderURL
@ -130,8 +136,11 @@ struct MinecraftContentItem: Identifiable, Hashable, Sendable {
self.lastPlayedDate = lastPlayedDate self.lastPlayedDate = lastPlayedDate
self.modifiedDate = modifiedDate self.modifiedDate = modifiedDate
self.sizeBytes = sizeBytes self.sizeBytes = sizeBytes
self.packUUID = packUUID?.lowercased()
self.packVersion = packVersion
self.packReferences = packReferences self.packReferences = packReferences
self.metadataLoaded = metadataLoaded self.metadataLoaded = metadataLoaded
self.sizeLoaded = sizeLoaded
} }
nonisolated var folderID: String { nonisolated var folderID: String {

View File

@ -11,7 +11,13 @@ struct MinecraftSource: Identifiable, Hashable, Sendable {
let id: URL let id: URL
let folderURL: URL let folderURL: URL
var displayName: String var displayName: String
var items: [MinecraftContentItem] var displayItems: [MinecraftContentItem]
var rawItems: [MinecraftContentItem]
var logicalPacks: [LogicalPack]
var logicalWorlds: [LogicalWorld]
var packInstances: [PackInstance]
var worldPackRelationships: [WorldPackRelationship]
var snapshot: SourceSnapshot?
var isScanning: Bool var isScanning: Bool
var scanStatus: String var scanStatus: String
var scanError: String? var scanError: String?
@ -24,7 +30,13 @@ struct MinecraftSource: Identifiable, Hashable, Sendable {
self.id = normalizedURL self.id = normalizedURL
self.folderURL = normalizedURL self.folderURL = normalizedURL
self.displayName = normalizedURL.lastPathComponent self.displayName = normalizedURL.lastPathComponent
self.items = [] self.displayItems = []
self.rawItems = []
self.logicalPacks = []
self.logicalWorlds = []
self.packInstances = []
self.worldPackRelationships = []
self.snapshot = nil
self.isScanning = false self.isScanning = false
self.scanStatus = "" self.scanStatus = ""
self.scanError = nil self.scanError = nil
@ -34,6 +46,83 @@ struct MinecraftSource: Identifiable, Hashable, Sendable {
} }
var itemCount: Int { var itemCount: Int {
items.count displayItems.count
}
var items: [MinecraftContentItem] {
displayItems
}
func rawItem(withID itemID: URL) -> MinecraftContentItem? {
rawItems.first(where: { $0.id == itemID })
}
func logicalPack(forRepresentativeItemID itemID: URL) -> LogicalPack? {
logicalPacks.first(where: { $0.representativeItemID == itemID })
}
func logicalWorld(forItemID itemID: URL) -> LogicalWorld? {
logicalWorlds.first(where: { $0.itemID == itemID })
}
func packInstances(for logicalPackID: PackIdentity) -> [PackInstance] {
packInstances.filter { $0.logicalPackID == logicalPackID }
}
func worldsUsingPack(_ logicalPackID: PackIdentity) -> [MinecraftContentItem] {
worldPackRelationships
.filter { $0.logicalPackID == logicalPackID }
.compactMap { rawItem(withID: $0.worldItemID) }
.uniqued(by: \.id)
.sorted(by: WorldScanner.sortItems)
}
func resolvedPackReferences(for worldItemID: URL, type: MinecraftContentType) -> [ContentPackReference] {
worldPackRelationships
.filter { $0.worldItemID == worldItemID && $0.reference.type == type }
.compactMap { relationship in
if let logicalPackID = relationship.logicalPackID,
let logicalPack = logicalPacks.first(where: { $0.id == logicalPackID }),
let representativeItem = rawItem(withID: logicalPack.representativeItemID) {
return ContentPackReference(
name: logicalPack.displayName,
type: logicalPack.contentType,
iconURL: representativeItem.iconURL,
uuid: logicalPack.uuid,
version: logicalPack.version,
source: relationship.reference.source
)
}
return relationship.reference
}
.uniqued(by: \.id)
}
private func shouldIncludeAsStandalone(_ item: MinecraftContentItem) -> Bool {
switch item.contentType {
case .world, .behaviorPack, .resourcePack:
return false
case .skinPack, .worldTemplate:
return true
}
}
}
private extension Array {
func uniqued<Key: Hashable>(by keyPath: KeyPath<Element, Key>) -> [Element] {
var seen = Set<Key>()
var result: [Element] = []
for element in self {
let key = element[keyPath: keyPath]
guard seen.insert(key).inserted else {
continue
}
result.append(element)
}
return result
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,22 @@
import Foundation import Foundation
enum WorldScanner { enum WorldScanner {
nonisolated static func loadSize(for item: MinecraftContentItem) -> MinecraftContentItem {
let fileManager = FileManager.default
var sizedItem = item
sizedItem.sizeBytes = folderSize(at: item.folderURL, fileManager: fileManager)
sizedItem.sizeLoaded = true
return sizedItem
}
nonisolated static func beginScanSession(for sourceRootURL: URL) async {
await packReferenceIndexStore.reset(for: sourceRootURL)
}
nonisolated static func endScanSession(for sourceRootURL: URL) async {
await packReferenceIndexStore.reset(for: sourceRootURL)
}
nonisolated static func discoverItems( nonisolated static func discoverItems(
in searchRootURL: URL, in searchRootURL: URL,
onDiscovered: @Sendable (MinecraftContentItem) -> Void = { _ in } onDiscovered: @Sendable (MinecraftContentItem) -> Void = { _ in }
@ -52,6 +68,16 @@ enum WorldScanner {
seenItemURLs.insert(itemURL) seenItemURLs.insert(itemURL)
discoveredItems.append(item) discoveredItems.append(item)
onDiscovered(item) onDiscovered(item)
if contentType == .world {
let embeddedPackItems = discoverEmbeddedPackItems(
in: childDirectory,
fileManager: fileManager,
seenItemURLs: &seenItemURLs
)
discoveredItems.append(contentsOf: embeddedPackItems)
embeddedPackItems.forEach(onDiscovered)
}
} }
} }
} }
@ -60,7 +86,7 @@ enum WorldScanner {
return discoveredItems return discoveredItems
} }
nonisolated static func enrich(item: MinecraftContentItem) -> MinecraftContentItem { nonisolated static func enrich(item: MinecraftContentItem) async -> MinecraftContentItem {
let fileManager = FileManager.default let fileManager = FileManager.default
var enrichedItem = item var enrichedItem = item
@ -68,9 +94,16 @@ enum WorldScanner {
enrichedItem.iconURL = iconURL(for: item, fileManager: fileManager) enrichedItem.iconURL = iconURL(for: item, fileManager: fileManager)
enrichedItem.lastPlayedDate = lastPlayedDate(for: item, fileManager: fileManager) enrichedItem.lastPlayedDate = lastPlayedDate(for: item, fileManager: fileManager)
enrichedItem.modifiedDate = modifiedDate(for: item.folderURL) enrichedItem.modifiedDate = modifiedDate(for: item.folderURL)
enrichedItem.sizeBytes = folderSize(at: item.folderURL, fileManager: fileManager) if let manifestMetadata = manifestMetadata(in: item.folderURL, fileManager: fileManager) {
enrichedItem.packReferences = packReferences(for: item, fileManager: fileManager) enrichedItem.packUUID = manifestMetadata.uuid
enrichedItem.packVersion = manifestMetadata.version
if !manifestMetadata.name.isEmpty {
enrichedItem.displayName = manifestMetadata.name
}
}
enrichedItem.packReferences = await packReferences(for: item, fileManager: fileManager)
enrichedItem.metadataLoaded = true enrichedItem.metadataLoaded = true
enrichedItem.sizeLoaded = false
return enrichedItem return enrichedItem
} }
@ -96,7 +129,7 @@ enum WorldScanner {
} }
} }
nonisolated private static func immediateChildDirectories(of directoryURL: URL, fileManager: FileManager) throws -> [URL] { nonisolated fileprivate static func immediateChildDirectories(of directoryURL: URL, fileManager: FileManager) throws -> [URL] {
let children = try fileManager.contentsOfDirectory( let children = try fileManager.contentsOfDirectory(
at: directoryURL, at: directoryURL,
includingPropertiesForKeys: [.isDirectoryKey], includingPropertiesForKeys: [.isDirectoryKey],
@ -122,6 +155,50 @@ enum WorldScanner {
} }
} }
nonisolated private static func discoverEmbeddedPackItems(
in worldDirectoryURL: URL,
fileManager: FileManager,
seenItemURLs: inout Set<URL>
) -> [MinecraftContentItem] {
let embeddedCollections: [(MinecraftContentType, URL)] = [
(.behaviorPack, worldDirectoryURL.appendingPathComponent("behavior_packs", isDirectory: true)),
(.resourcePack, worldDirectoryURL.appendingPathComponent("resource_packs", isDirectory: true))
]
var embeddedItems: [MinecraftContentItem] = []
for (contentType, collectionURL) in embeddedCollections {
guard
fileManager.fileExists(atPath: collectionURL.path),
let childDirectories = try? immediateChildDirectories(of: collectionURL, fileManager: fileManager)
else {
continue
}
for childDirectory in childDirectories {
let itemURL = childDirectory.standardizedFileURL
guard !seenItemURLs.contains(itemURL) else {
continue
}
guard isCandidateItem(at: childDirectory, type: contentType, fileManager: fileManager) else {
continue
}
let item = MinecraftContentItem(
folderURL: childDirectory,
folderName: childDirectory.lastPathComponent,
contentType: contentType,
collectionRootURL: collectionURL
)
seenItemURLs.insert(itemURL)
embeddedItems.append(item)
}
}
return embeddedItems
}
nonisolated private static func displayName(for item: MinecraftContentItem, fileManager: FileManager) -> String { nonisolated private static func displayName(for item: MinecraftContentItem, fileManager: FileManager) -> String {
switch item.contentType { switch item.contentType {
case .world: case .world:
@ -145,19 +222,7 @@ enum WorldScanner {
} }
nonisolated private static func manifestName(in directoryURL: URL, fileManager: FileManager) -> String? { nonisolated private static func manifestName(in directoryURL: URL, fileManager: FileManager) -> String? {
let manifestURL = directoryURL.appendingPathComponent("manifest.json") manifestMetadata(in: directoryURL, fileManager: fileManager)?.name
guard
fileManager.fileExists(atPath: manifestURL.path),
let data = try? Data(contentsOf: manifestURL),
let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let header = jsonObject["header"] as? [String: Any],
let name = (header["name"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!name.isEmpty
else {
return nil
}
return name
} }
nonisolated private static func packIconURL(in directoryURL: URL, fileManager: FileManager) -> URL? { nonisolated private static func packIconURL(in directoryURL: URL, fileManager: FileManager) -> URL? {
@ -235,10 +300,10 @@ enum WorldScanner {
return totalSize return totalSize
} }
nonisolated private static func packReferences(for item: MinecraftContentItem, fileManager: FileManager) -> [ContentPackReference] { nonisolated private static func packReferences(for item: MinecraftContentItem, fileManager: FileManager) async -> [ContentPackReference] {
switch item.contentType { switch item.contentType {
case .world: case .world:
var references = referencedWorldPacks(for: item, fileManager: fileManager) var references = await referencedWorldPacks(for: item, fileManager: fileManager)
references.append(contentsOf: embeddedWorldPacks(for: item, fileManager: fileManager)) references.append(contentsOf: embeddedWorldPacks(for: item, fileManager: fileManager))
return uniquePackReferences(references) return uniquePackReferences(references)
case .behaviorPack, .resourcePack, .skinPack, .worldTemplate: case .behaviorPack, .resourcePack, .skinPack, .worldTemplate:
@ -246,14 +311,14 @@ enum WorldScanner {
} }
} }
nonisolated private static func referencedWorldPacks(for item: MinecraftContentItem, fileManager: FileManager) -> [ContentPackReference] { nonisolated private static func referencedWorldPacks(for item: MinecraftContentItem, fileManager: FileManager) async -> [ContentPackReference] {
let behaviorReferences = packReferences( let behaviorReferences = await packReferences(
fromWorldReferenceFileNamed: "world_behavior_packs.json", fromWorldReferenceFileNamed: "world_behavior_packs.json",
type: .behaviorPack, type: .behaviorPack,
worldFolderURL: item.folderURL, worldFolderURL: item.folderURL,
fileManager: fileManager fileManager: fileManager
) )
let resourceReferences = packReferences( let resourceReferences = await packReferences(
fromWorldReferenceFileNamed: "world_resource_packs.json", fromWorldReferenceFileNamed: "world_resource_packs.json",
type: .resourcePack, type: .resourcePack,
worldFolderURL: item.folderURL, worldFolderURL: item.folderURL,
@ -289,7 +354,7 @@ enum WorldScanner {
type: MinecraftContentType, type: MinecraftContentType,
worldFolderURL: URL, worldFolderURL: URL,
fileManager: FileManager fileManager: FileManager
) -> [ContentPackReference] { ) async -> [ContentPackReference] {
let fileURL = worldFolderURL.appendingPathComponent(filename) let fileURL = worldFolderURL.appendingPathComponent(filename)
guard guard
fileManager.fileExists(atPath: fileURL.path), fileManager.fileExists(atPath: fileURL.path),
@ -299,19 +364,24 @@ enum WorldScanner {
return [] return []
} }
return jsonObject.compactMap { entry in var references: [ContentPackReference] = []
for entry in jsonObject {
let uuid = (entry["pack_id"] as? String)?.lowercased() let uuid = (entry["pack_id"] as? String)?.lowercased()
let version = versionString(from: entry["version"]) let version = versionString(from: entry["version"])
let resolvedPack = uuid.flatMap { let resolvedPack: ContentPackReference?
resolvedPackReference( if let uuid {
uuid: $0, resolvedPack = await resolvedPackReference(
uuid: uuid,
type: type, type: type,
worldCollectionRootURL: worldFolderURL.deletingLastPathComponent(), worldCollectionRootURL: worldFolderURL.deletingLastPathComponent()
fileManager: fileManager
) )
} else {
resolvedPack = nil
} }
let fallbackName = resolvedPack?.name ?? uuid ?? "Referenced Pack" let fallbackName = resolvedPack?.name ?? uuid ?? "Referenced Pack"
return ContentPackReference( references.append(
ContentPackReference(
name: fallbackName, name: fallbackName,
type: type, type: type,
iconURL: resolvedPack?.iconURL, iconURL: resolvedPack?.iconURL,
@ -319,7 +389,10 @@ enum WorldScanner {
version: resolvedPack?.version ?? version, version: resolvedPack?.version ?? version,
source: .referencedByWorld source: .referencedByWorld
) )
)
} }
return references
} }
nonisolated private static func embeddedPackReferences( nonisolated private static func embeddedPackReferences(
@ -344,34 +417,22 @@ enum WorldScanner {
} }
} }
nonisolated private static func packReference( nonisolated fileprivate static func packReference(
fromPackFolder directoryURL: URL, fromPackFolder directoryURL: URL,
type: MinecraftContentType, type: MinecraftContentType,
source: PackSource, source: PackSource,
fileManager: FileManager fileManager: FileManager
) -> ContentPackReference? { ) -> ContentPackReference? {
let manifestURL = directoryURL.appendingPathComponent("manifest.json") guard let metadata = manifestMetadata(in: directoryURL, fileManager: fileManager) else {
guard
fileManager.fileExists(atPath: manifestURL.path),
let data = try? Data(contentsOf: manifestURL),
let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else {
return nil return nil
} }
let header = jsonObject["header"] as? [String: Any]
let name = ((header?["name"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)).flatMap {
$0.isEmpty ? nil : $0
} ?? directoryURL.lastPathComponent
let uuid = (header?["uuid"] as? String)?.lowercased()
let version = versionString(from: header?["version"])
return ContentPackReference( return ContentPackReference(
name: name, name: metadata.name,
type: type, type: type,
iconURL: packIconURL(in: directoryURL, fileManager: fileManager), iconURL: packIconURL(in: directoryURL, fileManager: fileManager),
uuid: uuid, uuid: metadata.uuid,
version: version, version: metadata.version,
source: source source: source
) )
} }
@ -379,37 +440,17 @@ enum WorldScanner {
nonisolated private static func resolvedPackReference( nonisolated private static func resolvedPackReference(
uuid: String, uuid: String,
type: MinecraftContentType, type: MinecraftContentType,
worldCollectionRootURL: URL, worldCollectionRootURL: URL
fileManager: FileManager ) async -> ContentPackReference? {
) -> ContentPackReference? {
let siblingCollectionURL = worldCollectionRootURL let siblingCollectionURL = worldCollectionRootURL
.deletingLastPathComponent() .deletingLastPathComponent()
.appendingPathComponent(type.collectionFolderName, isDirectory: true) .appendingPathComponent(type.collectionFolderName, isDirectory: true)
guard return await packReferenceIndexStore.reference(
fileManager.fileExists(atPath: siblingCollectionURL.path), forUUID: uuid,
let childDirectories = try? immediateChildDirectories(of: siblingCollectionURL, fileManager: fileManager)
else {
return nil
}
for childDirectory in childDirectories {
guard
let reference = packReference(
fromPackFolder: childDirectory,
type: type, type: type,
source: .foundInCollection, in: siblingCollectionURL
fileManager: fileManager )
),
reference.uuid == uuid
else {
continue
}
return reference
}
return nil
} }
nonisolated private static func versionString(from value: Any?) -> String? { nonisolated private static func versionString(from value: Any?) -> String? {
@ -434,6 +475,28 @@ enum WorldScanner {
return nil return nil
} }
nonisolated private static func manifestMetadata(in directoryURL: URL, fileManager: FileManager) -> ManifestMetadata? {
let manifestURL = directoryURL.appendingPathComponent("manifest.json")
guard
fileManager.fileExists(atPath: manifestURL.path),
let data = try? Data(contentsOf: manifestURL),
let jsonObject = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let header = jsonObject["header"] as? [String: Any]
else {
return nil
}
let name = ((header["name"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)).flatMap {
$0.isEmpty ? nil : $0
} ?? directoryURL.lastPathComponent
return ManifestMetadata(
name: name,
uuid: (header["uuid"] as? String)?.lowercased(),
version: versionString(from: header["version"])
)
}
nonisolated private static func uniquePackReferences(_ references: [ContentPackReference]) -> [ContentPackReference] { nonisolated private static func uniquePackReferences(_ references: [ContentPackReference]) -> [ContentPackReference] {
var seen = Set<String>() var seen = Set<String>()
var uniqueReferences: [ContentPackReference] = [] var uniqueReferences: [ContentPackReference] = []
@ -457,3 +520,62 @@ enum WorldScanner {
} }
} }
} }
private struct ManifestMetadata {
let name: String
let uuid: String?
let version: String?
}
private actor PackReferenceIndexStore {
private var referencesByCollectionURL: [URL: [String: ContentPackReference]] = [:]
func reset(for sourceRootURL: URL) {
let sourceRootPath = sourceRootURL.standardizedFileURL.path
referencesByCollectionURL = referencesByCollectionURL.filter { collectionURL, _ in
!collectionURL.standardizedFileURL.path.hasPrefix(sourceRootPath + "/")
}
}
func reference(forUUID uuid: String, type: MinecraftContentType, in collectionURL: URL) -> ContentPackReference? {
let normalizedCollectionURL = collectionURL.standardizedFileURL
if let cachedReferences = referencesByCollectionURL[normalizedCollectionURL] {
return cachedReferences[uuid]
}
let fileManager = FileManager.default
guard
fileManager.fileExists(atPath: normalizedCollectionURL.path),
let childDirectories = try? WorldScanner.immediateChildDirectories(
of: normalizedCollectionURL,
fileManager: fileManager
)
else {
referencesByCollectionURL[normalizedCollectionURL] = [:]
return nil
}
var referencesByUUID: [String: ContentPackReference] = [:]
for childDirectory in childDirectories {
guard
let reference = WorldScanner.packReference(
fromPackFolder: childDirectory,
type: type,
source: .foundInCollection,
fileManager: fileManager
),
let referenceUUID = reference.uuid
else {
continue
}
referencesByUUID[referenceUUID] = reference
}
referencesByCollectionURL[normalizedCollectionURL] = referencesByUUID
return referencesByUUID[uuid]
}
}
private let packReferenceIndexStore = PackReferenceIndexStore()

View File

@ -15,6 +15,7 @@ struct World_Manager_for_MinecraftApp: App {
.tint(Color("AccentColor")) .tint(Color("AccentColor"))
.background(WindowChromeConfigurator()) .background(WindowChromeConfigurator())
} }
.defaultSize(width: 1520, height: 980)
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
.windowToolbarStyle(.unified(showsTitle: false)) .windowToolbarStyle(.unified(showsTitle: false))
} }

View File

@ -5,13 +5,157 @@
// Created by John Burwell on 2026-05-25. // Created by John Burwell on 2026-05-25.
// //
import Foundation
import Testing import Testing
@testable import World_Manager_for_Minecraft @testable import World_Manager_for_Minecraft
@MainActor
struct World_Manager_for_MinecraftTests { struct World_Manager_for_MinecraftTests {
@Test func example() async throws { @Test func packIdentityUsesUUIDAndVersion() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions. let first = PackIdentity(
type: .behaviorPack,
uuid: "ABC-123",
version: "1.0.0",
fallbackName: "Pack A",
fallbackLocationHint: "behavior_packs/pack-a"
)
let second = PackIdentity(
type: .behaviorPack,
uuid: "abc-123",
version: "1.0.0",
fallbackName: "Different Name",
fallbackLocationHint: "minecraftWorlds/world/behavior_packs/copy"
)
let third = PackIdentity(
type: .behaviorPack,
uuid: "abc-123",
version: "2.0.0",
fallbackName: "Pack A",
fallbackLocationHint: "behavior_packs/pack-a-v2"
)
#expect(first == second)
#expect(first != third)
#expect(first.isSuspicious == false)
}
@Test func minecraftSourceItemsUseLogicalPackRepresentative() async throws {
let sourceURL = URL(fileURLWithPath: "/tmp/source")
let worldURL = sourceURL.appendingPathComponent("minecraftWorlds/WorldA", isDirectory: true)
let topLevelPackURL = sourceURL.appendingPathComponent("behavior_packs/PackA", isDirectory: true)
let embeddedPackURL = worldURL.appendingPathComponent("behavior_packs/PackA", isDirectory: true)
let world = MinecraftContentItem(
folderURL: worldURL,
folderName: "WorldA",
contentType: .world,
collectionRootURL: sourceURL.appendingPathComponent("minecraftWorlds", isDirectory: true)
)
let topLevelPack = MinecraftContentItem(
folderURL: topLevelPackURL,
folderName: "PackA",
contentType: .behaviorPack,
collectionRootURL: sourceURL.appendingPathComponent("behavior_packs", isDirectory: true),
displayName: "Pack A"
)
let embeddedPack = MinecraftContentItem(
folderURL: embeddedPackURL,
folderName: "PackA",
contentType: .behaviorPack,
collectionRootURL: worldURL.appendingPathComponent("behavior_packs", isDirectory: true),
displayName: "Pack A"
)
let packID = PackIdentity(
type: .behaviorPack,
uuid: "pack-a",
version: "1.0.0",
fallbackName: "Pack A",
fallbackLocationHint: "behavior_packs/PackA"
)
var source = MinecraftSource(folderURL: sourceURL)
source.rawItems = [world, topLevelPack, embeddedPack]
source.logicalWorlds = [
LogicalWorld(
id: world.id,
itemID: world.id,
usedPackIDs: [packID],
unresolvedReferences: []
)
]
source.logicalPacks = [
LogicalPack(
id: packID,
contentType: .behaviorPack,
displayName: "Pack A",
uuid: "pack-a",
version: "1.0.0",
representativeItemID: topLevelPack.id,
instanceItemIDs: [topLevelPack.id, embeddedPack.id],
isSuspicious: false
)
]
let displayedItems = source.items
#expect(displayedItems.count == 2)
#expect(displayedItems.contains(where: { $0.id == world.id }))
#expect(displayedItems.contains(where: { $0.id == topLevelPack.id }))
#expect(displayedItems.contains(where: { $0.id == embeddedPack.id }) == false)
}
@Test func worldScannerResolvesReferencedPackFromIndexedCollection() async throws {
let fileManager = FileManager.default
let sourceURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let worldsURL = sourceURL.appendingPathComponent("minecraftWorlds", isDirectory: true)
let worldURL = worldsURL.appendingPathComponent("WorldA", isDirectory: true)
let packsURL = sourceURL.appendingPathComponent("behavior_packs", isDirectory: true)
let packURL = packsURL.appendingPathComponent("PackA", isDirectory: true)
try fileManager.createDirectory(at: worldURL, withIntermediateDirectories: true)
try fileManager.createDirectory(at: packURL, withIntermediateDirectories: true)
defer { try? fileManager.removeItem(at: sourceURL) }
let manifest = """
{
"header": {
"name": "Pack A",
"uuid": "pack-a",
"version": [1, 0, 0]
}
}
"""
let worldReference = """
[
{
"pack_id": "pack-a",
"version": [1, 0, 0]
}
]
"""
try manifest.write(to: packURL.appendingPathComponent("manifest.json"), atomically: true, encoding: .utf8)
try worldReference.write(
to: worldURL.appendingPathComponent("world_behavior_packs.json"),
atomically: true,
encoding: .utf8
)
let world = MinecraftContentItem(
folderURL: worldURL,
folderName: "WorldA",
contentType: .world,
collectionRootURL: worldsURL
)
await WorldScanner.beginScanSession(for: sourceURL)
let enrichedWorld = await WorldScanner.enrich(item: world)
#expect(enrichedWorld.packReferences.count == 1)
#expect(enrichedWorld.packReferences.first?.name == "Pack A")
#expect(enrichedWorld.packReferences.first?.uuid == "pack-a")
#expect(enrichedWorld.packReferences.first?.version == "1.0.0")
} }
} }