180 lines
6.1 KiB
Swift
180 lines
6.1 KiB
Swift
// SPDX-FileCopyrightText: 2026 John Burwell and contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import Foundation
|
|
|
|
struct MinecraftManifestMetadata: Sendable, Hashable {
|
|
let name: String
|
|
let uuid: String?
|
|
let version: String?
|
|
let minimumEngineVersion: String?
|
|
}
|
|
|
|
enum MinecraftContentMetadataReader {
|
|
nonisolated static func displayName(
|
|
for directoryURL: URL,
|
|
contentType: MinecraftContentType,
|
|
fallbackName: String,
|
|
fileManager: FileManager = .default
|
|
) -> String {
|
|
switch contentType {
|
|
case .world:
|
|
let levelNameURL = directoryURL.appendingPathComponent("levelname.txt")
|
|
guard
|
|
let name = try? String(contentsOf: levelNameURL, encoding: .utf8)
|
|
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
!name.isEmpty
|
|
else {
|
|
return fallbackName
|
|
}
|
|
|
|
return name
|
|
case .behaviorPack, .resourcePack, .skinPack, .worldTemplate:
|
|
if let manifestName = manifestMetadata(in: directoryURL, fileManager: fileManager)?.name {
|
|
return manifestName
|
|
}
|
|
|
|
return fallbackName
|
|
}
|
|
}
|
|
|
|
nonisolated static func iconURL(
|
|
for directoryURL: URL,
|
|
contentType: MinecraftContentType,
|
|
fileManager: FileManager = .default
|
|
) -> URL? {
|
|
let candidateNames: [String]
|
|
|
|
switch contentType {
|
|
case .world:
|
|
candidateNames = ["world_icon.jpeg", "world_icon.jpg", "world_icon.png"]
|
|
case .behaviorPack, .resourcePack, .skinPack, .worldTemplate:
|
|
candidateNames = ["pack_icon.png", "pack_icon.jpeg", "pack_icon.jpg"]
|
|
}
|
|
|
|
for candidateName in candidateNames {
|
|
let candidateURL = directoryURL.appendingPathComponent(candidateName)
|
|
if fileManager.fileExists(atPath: candidateURL.path) {
|
|
return candidateURL
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
nonisolated static func packIconURL(
|
|
in directoryURL: URL,
|
|
fileManager: FileManager = .default
|
|
) -> URL? {
|
|
let candidateNames = ["pack_icon.png", "pack_icon.jpeg", "pack_icon.jpg"]
|
|
|
|
for candidateName in candidateNames {
|
|
let candidateURL = directoryURL.appendingPathComponent(candidateName)
|
|
if fileManager.fileExists(atPath: candidateURL.path) {
|
|
return candidateURL
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
nonisolated static func worldMetadata(
|
|
in directoryURL: URL,
|
|
fileManager: FileManager = .default
|
|
) -> WorldMetadata? {
|
|
let levelDatURL = directoryURL.appendingPathComponent("level.dat")
|
|
guard fileManager.fileExists(atPath: levelDatURL.path) else {
|
|
return nil
|
|
}
|
|
|
|
return BedrockLevelMetadataDecoder.decode(fromLevelDatAt: levelDatURL)
|
|
}
|
|
|
|
nonisolated static func manifestMetadata(
|
|
in directoryURL: URL,
|
|
fileManager: FileManager = .default
|
|
) -> MinecraftManifestMetadata? {
|
|
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 MinecraftManifestMetadata(
|
|
name: name,
|
|
uuid: (header["uuid"] as? String)?.lowercased(),
|
|
version: versionString(from: header["version"]),
|
|
minimumEngineVersion: versionString(from: header["min_engine_version"])
|
|
)
|
|
}
|
|
|
|
nonisolated static func versionString(from value: Any?) -> String? {
|
|
if let versionString = value as? String, !versionString.isEmpty {
|
|
return versionString
|
|
}
|
|
|
|
if let versionArray = value as? [Any] {
|
|
let components = versionArray.compactMap { component -> String? in
|
|
if let intComponent = component as? Int {
|
|
return String(intComponent)
|
|
}
|
|
if let stringComponent = component as? String {
|
|
return stringComponent
|
|
}
|
|
return nil
|
|
}
|
|
|
|
return components.isEmpty ? nil : components.joined(separator: ".")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
nonisolated static func inferredPackContentType(
|
|
for directoryURL: URL,
|
|
fileManager: FileManager = .default
|
|
) -> MinecraftContentType {
|
|
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]
|
|
else {
|
|
return .behaviorPack
|
|
}
|
|
|
|
if let modules = jsonObject["modules"] as? [[String: Any]] {
|
|
let normalizedTypes = modules.compactMap { ($0["type"] as? String)?.lowercased() }
|
|
if normalizedTypes.contains("resources") || normalizedTypes.contains("client_data") {
|
|
return .resourcePack
|
|
}
|
|
if normalizedTypes.contains("skin_pack") {
|
|
return .skinPack
|
|
}
|
|
if normalizedTypes.contains("data") || normalizedTypes.contains("script") || normalizedTypes.contains("javascript") {
|
|
return .behaviorPack
|
|
}
|
|
}
|
|
|
|
if let metadata = jsonObject["metadata"] as? [String: Any],
|
|
let productType = (metadata["product_type"] as? String)?.lowercased() {
|
|
if productType.contains("skin") {
|
|
return .skinPack
|
|
}
|
|
if productType.contains("resource") {
|
|
return .resourcePack
|
|
}
|
|
}
|
|
|
|
return .behaviorPack
|
|
}
|
|
}
|