Skip to content

Commit c89f10a

Browse files
authored
feat: 获取并显示实例安装的资源 (#202)
1 parent 2c2c963 commit c89f10a

56 files changed

Lines changed: 1583 additions & 138 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// Archive+Entry.swift
3+
// PCL.Mac
4+
//
5+
// Created by AnemoFlower on 2026/6/7.
6+
//
7+
8+
import Foundation
9+
import ZIPFoundation
10+
11+
public extension Archive {
12+
/// 解压归档中的某个 `Entry`。
13+
func extract(_ entry: Entry) throws -> Data {
14+
var entryData: Data = .init()
15+
_ = try extract(entry, consumer: { data in
16+
entryData.append(data)
17+
})
18+
return entryData
19+
}
20+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// String+Template.swift
3+
// PCL.Mac
4+
//
5+
// Created by AnemoFlower on 2026/6/8.
6+
//
7+
8+
public extension String {
9+
func replacingPlaceholders(with values: [String: String], dollarPrefix: Bool = true) -> String {
10+
var s: String = self
11+
for key in values.keys {
12+
s = s.replacingOccurrences(of: (dollarPrefix ? "$" : "") + "{\(key)}", with: values[key]!)
13+
}
14+
return s
15+
}
16+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// URL+Utils.swift
3+
// PCL.Mac
4+
//
5+
// Created by AnemoFlower on 2026/6/16.
6+
//
7+
8+
import Foundation
9+
10+
public extension URL {
11+
func deletingAllPathExtensions() -> URL {
12+
var url = self
13+
while url.pathExtension != "" {
14+
url = url.deletingPathExtension()
15+
}
16+
return url
17+
}
18+
}

PCL.Mac.Core/Minecraft/Launch/MinecraftLauncher.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public class MinecraftLauncher {
6363
}
6464
arguments.append(manifest.mainClass)
6565
arguments.append(contentsOf: manifest.gameArguments.flatMap { $0.rules.allSatisfy { $0.test(with: options) } ? $0.value : [] })
66-
arguments = arguments.map { Utils.replace($0, withValues: values) }
66+
arguments = arguments.map { $0.replacingPlaceholders(with: values) }
6767
process.arguments = arguments
6868

6969
let pipe: Pipe = .init()

PCL.Mac.Core/Models/CurseForge/CurseForgeMod.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
import Foundation
99

1010
public struct CurseForgeMod: Codable {
11+
public let id: Int32
1112
public let name: String
13+
public let summary: String
14+
public let logo: CurseForgeModAsset
1215
public let classId: Int?
1316

14-
public var projectType: ProjectType? {
17+
public var projectType: ResourceType? {
1518
switch classId {
1619
case 12: .resourcepack
1720
case 6: .mod
@@ -22,6 +25,17 @@ public struct CurseForgeMod: Codable {
2225
}
2326
}
2427

28+
public struct CurseForgeModAsset: Codable {
29+
public let id: Int32
30+
public let title: String
31+
public let url: URL
32+
public let thumbnailURL: URL
33+
34+
private enum CodingKeys: String, CodingKey {
35+
case id, title, url, thumbnailURL = "thumbnailUrl"
36+
}
37+
}
38+
2539
public struct CurseForgeModFile: Codable {
2640
public struct FileHash: Codable {
2741
public let value: String

PCL.Mac.Core/Models/ModLoader.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import Foundation
99

10-
public enum ModLoader: String, CustomStringConvertible {
10+
public enum ModLoader: String, Codable, CustomStringConvertible {
1111
case fabric, forge, neoforge
1212

1313
public var index: Int {

PCL.Mac.Core/Models/Modpack/ModpackIndex.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,7 @@ public extension ModpackIndex {
9898

9999
public var path: String? {
100100
guard let cachedMod, let cachedModFile, let type = cachedMod.projectType else { return nil }
101-
guard let directory: String = switch type {
102-
case .mod: "mods"
103-
case .modpack: nil
104-
case .resourcepack: "resourcepacks"
105-
case .shader: "shaderpacks"
106-
} else { return nil }
101+
guard let directory = type.saveDirectory else { return nil }
107102
return "\(directory)/\(cachedModFile.fileName)"
108103
}
109104

PCL.Mac.Core/Models/Modrinth/ModrinthProject.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public struct ModrinthProject: Decodable, Identifiable, Hashable, Equatable {
2424

2525
public let id: String
2626
public let slug: String
27-
public let type: ProjectType
27+
public let type: ResourceType
2828
public let title: String
2929
public let description: String
3030
public let iconURL: URL?
@@ -40,7 +40,7 @@ public struct ModrinthProject: Decodable, Identifiable, Hashable, Equatable {
4040
let container = try decoder.container(keyedBy: CodingKeys.self)
4141
self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? container.decode(String.self, forKey: .projectId)
4242
self.slug = try container.decode(String.self, forKey: .slug)
43-
self.type = try container.decode(ProjectType.self, forKey: .type)
43+
self.type = try container.decode(ResourceType.self, forKey: .type)
4444
self.title = try container.decode(String.self, forKey: .title)
4545
self.description = try container.decode(String.self, forKey: .description)
4646
self.iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL).flatMap(URL.init(string:))

PCL.Mac.Core/Models/Resource.swift

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,62 @@
77

88
import Foundation
99

10-
public enum ProjectType: String, Codable {
10+
public enum ResourceType: String, Codable {
1111
case mod, modpack, resourcepack, shader
12+
13+
public var saveDirectory: String? {
14+
switch self {
15+
case .mod: "mods"
16+
case .modpack: nil
17+
case .resourcepack: "resourcepacks"
18+
case .shader: "shaderpacks"
19+
}
20+
}
21+
}
22+
23+
public struct Resource: Codable, Hashable, Equatable {
24+
public enum Source: Codable, Hashable, Equatable {
25+
case modrinth(projectId: String)
26+
case curseforge(id: Int32)
27+
}
28+
29+
public enum Icon: Codable, Hashable, Equatable {
30+
case archiveEntry(path: String, globalHash: String)
31+
case network(url: URL)
32+
}
33+
34+
public let id: UUID = .init()
35+
public let type: ResourceType
36+
public let name: String
37+
public let version: String?
38+
public let description: String?
39+
public let icon: Icon?
40+
public let loaders: [ModLoader]
41+
public let tags: [String]
42+
public let sources: [Source]
43+
44+
public init(
45+
type: ResourceType,
46+
name: String,
47+
version: String?,
48+
description: String?,
49+
icon: Icon?,
50+
loaders: [ModLoader],
51+
tags: [String],
52+
sources: [Source]
53+
) {
54+
self.type = type
55+
self.name = name
56+
self.version = version
57+
self.description = description
58+
self.icon = icon
59+
self.loaders = loaders
60+
self.tags = tags
61+
self.sources = sources
62+
}
63+
64+
private enum CodingKeys: CodingKey {
65+
// 不持久化存储 id
66+
case type, name, version, description, icon, loaders, tags, sources
67+
}
1268
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// ResourceCache.swift
3+
// PCL.Mac
4+
//
5+
// Created by AnemoFlower on 2026/6/7.
6+
//
7+
8+
import Foundation
9+
10+
public class ResourceCache {
11+
public static var shared: ResourceCache!
12+
13+
private let cacheFileURL: URL
14+
private let iconCacheDirectory: URL
15+
private var cacheMap: [String: Resource]
16+
17+
public init(cacheDirectory: URL) {
18+
self.cacheFileURL = cacheDirectory.appending(path: "resource_cache.json")
19+
self.iconCacheDirectory = cacheDirectory.appending(path: "resource_icon")
20+
21+
if FileManager.default.fileExists(atPath: cacheFileURL.path) {
22+
do {
23+
let data = try Data(contentsOf: cacheFileURL)
24+
self.cacheMap = try JSONDecoder.shared.decode([String: Resource].self, from: data)
25+
} catch {
26+
err("缓存文件加载失败:\(error.localizedDescription)")
27+
try? FileManager.default.removeItem(at: cacheFileURL)
28+
self.cacheMap = [:]
29+
}
30+
} else {
31+
self.cacheMap = [:]
32+
}
33+
}
34+
35+
public func resource(forHash hash: String) -> Resource? { cacheMap[hash] }
36+
37+
public func store(_ resource: Resource, forHash hash: String) { cacheMap[hash] = resource }
38+
39+
public func icon(forHash globalHash: String) throws -> Data? {
40+
let url = url(of: globalHash)
41+
if FileManager.default.fileExists(atPath: url.path) {
42+
return try Data(contentsOf: url)
43+
}
44+
return nil
45+
}
46+
47+
public func store(_ iconData: Data, relativePath: String, fileHash: String) throws -> String {
48+
let globalHash = globalHash(of: relativePath, with: fileHash)
49+
let url = url(of: globalHash)
50+
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
51+
try iconData.write(to: url)
52+
return globalHash
53+
}
54+
55+
public func save() throws {
56+
try FileManager.default.createDirectory(at: cacheFileURL.deletingLastPathComponent(), withIntermediateDirectories: true)
57+
let data = try JSONEncoder.shared.encode(cacheMap)
58+
try data.write(to: cacheFileURL)
59+
}
60+
61+
62+
private func url(of globalHash: String) -> URL {
63+
return iconCacheDirectory.appending(path: "\(globalHash.prefix(2))/\(globalHash)")
64+
}
65+
66+
private func globalHash(of path: String, with fileHash: String) -> String {
67+
return (fileHash + path).sha1
68+
}
69+
}

0 commit comments

Comments
 (0)