world-manager/World Manager for Minecraft/Models/Content/MinecraftContentItem.swift
2026-06-01 20:50:52 -05:00

456 lines
14 KiB
Swift

// SPDX-FileCopyrightText: 2026 John Burwell and contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
typealias PlatformProviderID = String
nonisolated enum MinecraftEdition: String, CaseIterable, Hashable, Sendable, Codable {
case bedrock
case java
}
nonisolated enum MinecraftContentKind: String, CaseIterable, Hashable, Sendable, Codable {
case world
case behaviorPack
case resourcePack
case dataPack
case skinPack
case worldTemplate
case shaderPack
case mod
}
nonisolated enum JavaContentType: String, CaseIterable, Hashable, Sendable, Codable {
case world = "Java World"
case resourcePack = "Java Resource Pack"
case dataPack = "Java Data Pack"
case shaderPack = "Java Shader Pack"
case mod = "Java Mod"
nonisolated var kind: MinecraftContentKind {
switch self {
case .world:
return .world
case .resourcePack:
return .resourcePack
case .dataPack:
return .dataPack
case .shaderPack:
return .shaderPack
case .mod:
return .mod
}
}
}
nonisolated enum MinecraftContentType: String, CaseIterable, Hashable, Sendable, Codable {
case world = "World"
case behaviorPack = "Behavior Pack"
case resourcePack = "Resource Pack"
case skinPack = "Skin Pack"
case worldTemplate = "World Template"
nonisolated var collectionFolderName: String {
switch self {
case .world:
return "minecraftWorlds"
case .behaviorPack:
return "behavior_packs"
case .resourcePack:
return "resource_packs"
case .skinPack:
return "skin_packs"
case .worldTemplate:
return "world_templates"
}
}
nonisolated var kind: MinecraftContentKind {
switch self {
case .world:
return .world
case .behaviorPack:
return .behaviorPack
case .resourcePack:
return .resourcePack
case .skinPack:
return .skinPack
case .worldTemplate:
return .worldTemplate
}
}
nonisolated var archiveExtension: String {
switch self {
case .world:
return "mcworld"
case .behaviorPack, .resourcePack, .skinPack:
return "mcpack"
case .worldTemplate:
return "mctemplate"
}
}
nonisolated var exportTitle: String {
switch self {
case .world:
return "Minecraft World"
case .behaviorPack:
return "Behavior Pack"
case .resourcePack:
return "Resource Pack"
case .skinPack:
return "Skin Pack"
case .worldTemplate:
return "World Template"
}
}
}
nonisolated enum MinecraftPlatformContentType: Hashable, Sendable, Codable {
case bedrock(MinecraftContentType)
case java(JavaContentType)
nonisolated var edition: MinecraftEdition {
switch self {
case .bedrock:
return .bedrock
case .java:
return .java
}
}
nonisolated var kind: MinecraftContentKind {
switch self {
case .bedrock(let contentType):
return contentType.kind
case .java(let contentType):
return contentType.kind
}
}
nonisolated var displayName: String {
switch self {
case .bedrock(let contentType):
return contentType.rawValue
case .java(let contentType):
return contentType.rawValue
}
}
}
nonisolated struct ContentItemCapabilities: Hashable, Sendable, Codable {
var canRevealNativeContent: Bool
var canExportPortablePackage: Bool
var canShare: Bool
var portablePackageExtension: String?
nonisolated static func bedrock(contentType: MinecraftContentType) -> ContentItemCapabilities {
ContentItemCapabilities(
canRevealNativeContent: true,
canExportPortablePackage: true,
canShare: true,
portablePackageExtension: contentType.archiveExtension
)
}
nonisolated static func java(contentType: JavaContentType) -> ContentItemCapabilities {
let extensionName: String?
switch contentType {
case .world, .resourcePack, .dataPack, .shaderPack:
extensionName = "zip"
case .mod:
extensionName = "jar"
}
return ContentItemCapabilities(
canRevealNativeContent: true,
canExportPortablePackage: true,
canShare: true,
portablePackageExtension: extensionName
)
}
}
nonisolated enum PackSource: String, Hashable, Sendable, Codable {
case referencedByWorld
case embeddedInWorld
case foundInCollection
}
nonisolated struct ContentPackReference: Identifiable, Hashable, Sendable, Codable {
let id: String
let name: String
let type: MinecraftContentType
let iconURL: URL?
let uuid: String?
let version: String?
let source: PackSource
nonisolated init(
name: String,
type: MinecraftContentType,
iconURL: URL? = nil,
uuid: String? = nil,
version: String? = nil,
source: PackSource
) {
self.type = type
self.iconURL = iconURL
self.uuid = uuid?.lowercased()
self.version = version
self.source = source
self.name = name
self.id = [
type.rawValue,
self.uuid ?? name,
version ?? source.rawValue
].joined(separator: "::")
}
}
nonisolated struct WorldMetadata: Hashable, Sendable, Codable {
var gameMode: String?
var difficulty: String?
var seed: String?
var lastPlayedDate: Date?
var lastOpenedWithVersion: String?
var inventoryVersion: String?
var cheatsEnabled: Bool?
var commandsEnabled: Bool?
var educationFeaturesEnabled: Bool?
var coordinatesShown: Bool?
var keepInventory: Bool?
var mobGriefingEnabled: Bool?
var daylightCycleEnabled: Bool?
var weatherCycleEnabled: Bool?
var spawn: String?
var storageVersion: String?
var networkVersion: String?
}
nonisolated struct PackMetadataDetails: Hashable, Sendable, Codable {
var minimumEngineVersion: String?
}
nonisolated enum PlatformContentMetadata: Hashable, Sendable, Codable {
case bedrock(BedrockContentMetadata)
case java(JavaContentMetadata)
case none
}
nonisolated struct BedrockContentMetadata: Hashable, Sendable, Codable {
var world: WorldMetadata?
var packUUID: String?
var packVersion: String?
var packDetails: PackMetadataDetails?
var packReferences: [ContentPackReference]
nonisolated init(
world: WorldMetadata? = nil,
packUUID: String? = nil,
packVersion: String? = nil,
packDetails: PackMetadataDetails? = nil,
packReferences: [ContentPackReference] = []
) {
self.world = world
self.packUUID = packUUID?.lowercased()
self.packVersion = packVersion
self.packDetails = packDetails
self.packReferences = packReferences
}
}
nonisolated struct JavaContentMetadata: Hashable, Sendable, Codable {
var world: JavaWorldMetadata?
var pack: JavaPackMetadata?
var dataPacks: [JavaPackReference]
nonisolated init(
world: JavaWorldMetadata? = nil,
pack: JavaPackMetadata? = nil,
dataPacks: [JavaPackReference] = []
) {
self.world = world
self.pack = pack
self.dataPacks = dataPacks
}
}
nonisolated struct JavaWorldMetadata: Hashable, Sendable, Codable {
var dataVersion: String?
var gameMode: String?
var difficulty: String?
var seed: String?
var lastPlayedDate: Date?
}
nonisolated struct JavaPackMetadata: Hashable, Sendable, Codable {
var packFormat: Int?
var description: String?
}
nonisolated struct JavaPackReference: Identifiable, Hashable, Sendable, Codable {
let id: String
var name: String
var pathHint: String?
}
nonisolated struct MinecraftContentItem: Identifiable, Hashable, Sendable, Codable {
let id: URL
let folderURL: URL
let folderName: String
let contentType: MinecraftContentType
let sourceEdition: MinecraftEdition
let contentKind: MinecraftContentKind
let platformType: MinecraftPlatformContentType
let collectionRootURL: URL
var displayName: String
var iconURL: URL?
var hasKnownIcon: Bool
var lastPlayedDate: Date?
var modifiedDate: Date?
var sizeBytes: Int64?
var capabilities: ContentItemCapabilities
var platformMetadata: PlatformContentMetadata
var packUUID: String? {
didSet { syncBedrockMetadataFromCompatibilityFields() }
}
var packVersion: String? {
didSet { syncBedrockMetadataFromCompatibilityFields() }
}
var packMetadataDetails: PackMetadataDetails? {
didSet { syncBedrockMetadataFromCompatibilityFields() }
}
var packReferences: [ContentPackReference] {
didSet { syncBedrockMetadataFromCompatibilityFields() }
}
var worldMetadata: WorldMetadata? {
didSet { syncBedrockMetadataFromCompatibilityFields() }
}
var metadataLoaded: Bool
var previewLoaded: Bool
var sizeLoaded: Bool
nonisolated init(
folderURL: URL,
folderName: String,
contentType: MinecraftContentType,
sourceEdition: MinecraftEdition? = nil,
contentKind: MinecraftContentKind? = nil,
platformType: MinecraftPlatformContentType? = nil,
collectionRootURL: URL,
displayName: String? = nil,
iconURL: URL? = nil,
hasKnownIcon: Bool = false,
lastPlayedDate: Date? = nil,
modifiedDate: Date? = nil,
sizeBytes: Int64? = nil,
capabilities: ContentItemCapabilities? = nil,
platformMetadata: PlatformContentMetadata? = nil,
packUUID: String? = nil,
packVersion: String? = nil,
packMetadataDetails: PackMetadataDetails? = nil,
packReferences: [ContentPackReference] = [],
worldMetadata: WorldMetadata? = nil,
metadataLoaded: Bool = false,
previewLoaded: Bool = false,
sizeLoaded: Bool = false
) {
self.id = folderURL.standardizedFileURL
self.folderURL = folderURL
self.folderName = folderName
self.contentType = contentType
self.sourceEdition = sourceEdition ?? .bedrock
self.contentKind = contentKind ?? contentType.kind
self.platformType = platformType ?? .bedrock(contentType)
self.collectionRootURL = collectionRootURL
self.displayName = displayName ?? folderName
self.iconURL = iconURL
self.hasKnownIcon = hasKnownIcon
self.lastPlayedDate = lastPlayedDate
self.modifiedDate = modifiedDate
self.sizeBytes = sizeBytes
self.capabilities = capabilities ?? .bedrock(contentType: contentType)
self.platformMetadata = platformMetadata ?? .bedrock(
BedrockContentMetadata(
world: worldMetadata,
packUUID: packUUID,
packVersion: packVersion,
packDetails: packMetadataDetails,
packReferences: packReferences
)
)
self.packUUID = packUUID?.lowercased()
self.packVersion = packVersion
self.packMetadataDetails = packMetadataDetails
self.packReferences = packReferences
self.worldMetadata = worldMetadata
self.metadataLoaded = metadataLoaded
self.previewLoaded = previewLoaded
self.sizeLoaded = sizeLoaded
}
nonisolated mutating private func syncBedrockMetadataFromCompatibilityFields() {
guard sourceEdition == .bedrock else {
return
}
platformMetadata = .bedrock(
BedrockContentMetadata(
world: worldMetadata,
packUUID: packUUID,
packVersion: packVersion,
packDetails: packMetadataDetails,
packReferences: packReferences
)
)
}
nonisolated var folderID: String {
folderName
}
nonisolated var displayDate: Date? {
lastPlayedDate ?? modifiedDate
}
nonisolated var displayDateLabel: String {
lastPlayedDate == nil ? "Modified" : "Last Played"
}
nonisolated var searchText: String {
var values: [String] = [
displayName,
folderName,
folderURL.path,
contentType.rawValue
]
values.append(worldMetadata?.gameMode ?? "")
values.append(worldMetadata?.difficulty ?? "")
values.append(worldMetadata?.seed ?? "")
values.append(worldMetadata?.lastOpenedWithVersion ?? "")
values.append(packMetadataDetails?.minimumEngineVersion ?? "")
values.append(packReferences.map(\.name).joined(separator: " "))
values.append(packReferences.compactMap(\.uuid).joined(separator: " "))
return values
.filter { !$0.isEmpty }
.joined(separator: "\n")
}
nonisolated static func displaySort(_ lhs: MinecraftContentItem, _ rhs: MinecraftContentItem) -> Bool {
if lhs.contentType != rhs.contentType {
return lhs.contentType.rawValue.localizedStandardCompare(rhs.contentType.rawValue) == .orderedAscending
}
let displayNameOrder = lhs.displayName.localizedStandardCompare(rhs.displayName)
if displayNameOrder != .orderedSame {
return displayNameOrder == .orderedAscending
}
return lhs.folderName.localizedStandardCompare(rhs.folderName) == .orderedAscending
}
}