diff --git a/World Manager for Minecraft/Models/Sources/MinecraftSource.swift b/World Manager for Minecraft/Models/Sources/MinecraftSource.swift index 0c9997c..c1e00ab 100644 --- a/World Manager for Minecraft/Models/Sources/MinecraftSource.swift +++ b/World Manager for Minecraft/Models/Sources/MinecraftSource.swift @@ -13,6 +13,7 @@ struct MinecraftSource: Identifiable, Hashable, Sendable { var origin: MinecraftSourceOrigin var accessDescriptor: SourceAccessDescriptor var availability: SourceAvailability + var capabilities: SourceCapabilities var bookmarkData: Data? var displayName: String var displayItems: [MinecraftContentItem] @@ -56,6 +57,7 @@ struct MinecraftSource: Identifiable, Hashable, Sendable { refreshStrategy: resolvedOrigin.defaultRefreshStrategy ) self.availability = availability + self.capabilities = resolvedOrigin.defaultCapabilities self.bookmarkData = bookmarkData self.displayName = normalizedFolderURL.lastPathComponent self.displayItems = [] diff --git a/World Manager for Minecraft/Models/Sources/SourceCapabilities.swift b/World Manager for Minecraft/Models/Sources/SourceCapabilities.swift new file mode 100644 index 0000000..4c76070 --- /dev/null +++ b/World Manager for Minecraft/Models/Sources/SourceCapabilities.swift @@ -0,0 +1,26 @@ +// +// SourceCapabilities.swift +// World Manager for Minecraft +// +// Created by OpenAI on 2026-05-29. +// + +import Foundation + +struct SourceCapabilities: Hashable, Sendable, Codable { + var canScan: Bool = true + var canMaterializeItems: Bool = true + var canExportPortablePackages: Bool = true + + static let localFolder = SourceCapabilities( + canScan: true, + canMaterializeItems: true, + canExportPortablePackages: true + ) + + static let connectedDevice = SourceCapabilities( + canScan: true, + canMaterializeItems: true, + canExportPortablePackages: true + ) +} diff --git a/World Manager for Minecraft/Models/Sources/SourceOrigin.swift b/World Manager for Minecraft/Models/Sources/SourceOrigin.swift index f942ed9..f429096 100644 --- a/World Manager for Minecraft/Models/Sources/SourceOrigin.swift +++ b/World Manager for Minecraft/Models/Sources/SourceOrigin.swift @@ -77,6 +77,15 @@ nonisolated enum MinecraftSourceOrigin: Hashable, Sendable, Codable { return .staged } } + + nonisolated var defaultCapabilities: SourceCapabilities { + switch self { + case .localFolder: + return .localFolder + case .connectedDevice: + return .connectedDevice + } + } } nonisolated enum MinecraftSourceKind: String, Hashable, Sendable, Codable { diff --git a/World Manager for Minecraft/Services/Sources/Core/SourceLibrary.swift b/World Manager for Minecraft/Services/Sources/Core/SourceLibrary.swift index c1f47a0..ada92de 100644 --- a/World Manager for Minecraft/Services/Sources/Core/SourceLibrary.swift +++ b/World Manager for Minecraft/Services/Sources/Core/SourceLibrary.swift @@ -131,6 +131,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer source.bookmarkData = bookmarkData } source.accessDescriptor = sourceAccessMethod.accessDescriptor(for: source) + source.capabilities = source.origin.defaultCapabilities } startScan(for: normalizedURL, mode: .fullScan) return normalizedURL @@ -155,6 +156,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer existingSource.origin = source.origin existingSource.accessDescriptor = source.accessDescriptor existingSource.availability = source.availability + existingSource.capabilities = source.capabilities if existingSource.bookmarkData == nil { existingSource.bookmarkData = source.bookmarkData } @@ -165,6 +167,7 @@ final class SourceLibrary: ObservableObject, SourceScanSessionHosting, SourcePer } else { var resolvedSource = source resolvedSource.accessDescriptor = sourceAccessMethod.accessDescriptor(for: resolvedSource) + resolvedSource.capabilities = resolvedSource.origin.defaultCapabilities sources.append(resolvedSource) sources.sort { $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending } } diff --git a/World Manager for Minecraft/SourceAccess/ConnectedDevice/AppleMobileDevice/AppleMobileDeviceSourceAccess.swift b/World Manager for Minecraft/SourceAccess/ConnectedDevice/AppleMobileDevice/AppleMobileDeviceSourceAccess.swift index f3f675b..616106e 100644 --- a/World Manager for Minecraft/SourceAccess/ConnectedDevice/AppleMobileDevice/AppleMobileDeviceSourceAccess.swift +++ b/World Manager for Minecraft/SourceAccess/ConnectedDevice/AppleMobileDevice/AppleMobileDeviceSourceAccess.swift @@ -45,6 +45,11 @@ struct AppleMobileDeviceSourceAccess: ConnectedDeviceSourceAccessMethod { } } + nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities { + _ = source + return .connectedDevice + } + nonisolated func listConnectedDevices() async throws -> [ConnectedDevice] { let devices = try await AppleMobileDeviceAccess.connectedDevices() return devices.compactMap { device in diff --git a/World Manager for Minecraft/SourceAccess/Core/SourceAccessCoordinator.swift b/World Manager for Minecraft/SourceAccess/Core/SourceAccessCoordinator.swift index 777b7ea..d14c193 100644 --- a/World Manager for Minecraft/SourceAccess/Core/SourceAccessCoordinator.swift +++ b/World Manager for Minecraft/SourceAccess/Core/SourceAccessCoordinator.swift @@ -16,6 +16,7 @@ protocol SourceAccessMethod: Sendable { nonisolated var accessorIdentifier: SourceAccessorIdentifier { get } nonisolated func accessDescriptor(for source: MinecraftSource) -> SourceAccessDescriptor nonisolated func availability(for source: MinecraftSource) async -> SourceAvailability + nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities nonisolated func discoverItems( for source: MinecraftSource, mode: SourceDiscoveryMode, @@ -49,6 +50,10 @@ extension SourceAccessMethod { return .unknown } + nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities { + source.origin.defaultCapabilities + } + nonisolated func discoverItems( for source: MinecraftSource, mode: SourceDiscoveryMode, @@ -171,6 +176,10 @@ struct SourceAccessCoordinator: SourceAccessMethod { return await accessMethod(for: source).availability(for: source) } + nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities { + return await accessMethod(for: source).capabilities(for: source) + } + nonisolated func enrich(_ item: MinecraftContentItem, for source: MinecraftSource) async -> MinecraftContentItem { return await accessMethod(for: source).enrich(item, for: source) } diff --git a/World Manager for Minecraft/SourceAccess/LocalFolder/LocalFolderSourceAccess.swift b/World Manager for Minecraft/SourceAccess/LocalFolder/LocalFolderSourceAccess.swift index 2080508..0741818 100644 --- a/World Manager for Minecraft/SourceAccess/LocalFolder/LocalFolderSourceAccess.swift +++ b/World Manager for Minecraft/SourceAccess/LocalFolder/LocalFolderSourceAccess.swift @@ -43,6 +43,11 @@ struct LocalFolderSourceAccess: SourceAccessMethod { return FileManager.default.fileExists(atPath: candidateURL.path) ? .available : .unavailable } + nonisolated func capabilities(for source: MinecraftSource) async -> SourceCapabilities { + _ = source + return .localFolder + } + nonisolated func discoverItems( for source: MinecraftSource, mode: SourceDiscoveryMode, diff --git a/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift b/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift index cec7c8f..ac65960 100644 --- a/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift +++ b/World Manager for MinecraftTests/World_Manager_for_MinecraftTests.swift @@ -12,6 +12,33 @@ import Testing @MainActor struct World_Manager_for_MinecraftTests { + @Test func sourceOriginsExposeOutboundCapabilities() async throws { + let localSource = MinecraftSource(folderURL: URL(fileURLWithPath: "/tmp/local")) + #expect(localSource.capabilities == .localFolder) + + let device = ConnectedDevice( + udid: "device", + name: "Device", + productType: nil, + osVersion: nil, + connection: .usb, + trustState: .trusted + ) + let container = DeviceAppContainer( + deviceUDID: device.udid, + appID: "com.mojang.minecraftpe", + appName: "Minecraft", + accessMode: .documents, + minecraftFolderRelativePath: "Documents/games/com.mojang" + ) + let deviceSource = MinecraftSource( + folderURL: URL(fileURLWithPath: "/tmp/device"), + origin: .connectedDevice(device: device, container: container) + ) + + #expect(deviceSource.capabilities == .connectedDevice) + } + @Test func packIdentityUsesUUIDAndVersion() async throws { let first = PackIdentity( type: .behaviorPack,