minecraft-dev-starter 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +166 -0
- package/README.md +124 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +1521 -0
- package/package.json +54 -4
- package/src/client/Capsule.js +158 -0
- package/src/client/DetailTab.js +228 -0
- package/src/client/Flyout.js +191 -0
- package/src/client/LogTab.js +180 -0
- package/src/client/Skeleton.js +18 -0
- package/src/client/api.js +33 -0
- package/src/client/debug.js +27 -0
- package/src/client/format.js +89 -0
- package/src/client/h.js +8 -0
- package/src/client/icons.js +147 -0
- package/src/client/index.js +205 -0
- package/src/client/runtime.js +41 -0
- package/src/client/selection.js +154 -0
- package/src/client/state.js +233 -0
- package/src/client/store.js +34 -0
- package/src/client/styles.js +329 -0
- package/src/index.js +193 -0
- package/src/lib/api.js +186 -0
- package/src/lib/argv.js +66 -0
- package/src/lib/detect.js +460 -0
- package/src/lib/gradle.js +295 -0
- package/src/lib/java.js +204 -0
- package/src/lib/launches.js +495 -0
- package/src/lib/metadata.js +175 -0
- package/src/lib/scanner.js +131 -0
- package/src/lib/state.js +91 -0
- package/src/lib/text.js +92 -0
- package/src/lib/toml.js +281 -0
- package/src/tools.js +291 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工程识别:把一个目录翻译成结构化画像(loader / 版本 / 模块 / 可运行目标 / JDK / 证据)。
|
|
3
|
+
*
|
|
4
|
+
* 设计原则:
|
|
5
|
+
* - 一切结论附证据,读不出就报 unknown,不猜;
|
|
6
|
+
* - 任务目标来自构建脚本里的 `runs` 块,读不到时给"推测"标记;
|
|
7
|
+
* - 不执行 Gradle(代价与失败率见调研文档)。
|
|
8
|
+
* @module minecraft-dev-starter/lib/detect
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { basename, join } from 'node:path'
|
|
12
|
+
import { stat } from 'node:fs/promises'
|
|
13
|
+
import {
|
|
14
|
+
minecraftFromNeoVersion,
|
|
15
|
+
parseJavaLevel,
|
|
16
|
+
parseMinecraftVersion,
|
|
17
|
+
parsePluginIds,
|
|
18
|
+
parseRuns,
|
|
19
|
+
parseSettingsIncludes,
|
|
20
|
+
parseWrapperVersion,
|
|
21
|
+
} from './gradle.js'
|
|
22
|
+
import { dependencyRanges, locateMetadata, primaryMetadata } from './metadata.js'
|
|
23
|
+
import { readText, resolvePlaceholders, parseProperties } from './text.js'
|
|
24
|
+
import { gradleSupportsJava, pickJdk, requiredJava } from './java.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {{ file: string, kind: string, detail: string }} Evidence
|
|
28
|
+
* @typedef {{ task: string, label: string, kind: string, guessed: boolean }} Target
|
|
29
|
+
* @typedef {{ path: string, dir: string, platform: string, pluginIds: string[], targets: Target[] }} ModuleInfo
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** loader 判定:插件 id 正则 → loader。顺序有意义,先命中先得。 */
|
|
33
|
+
const LOADER_RULES = [
|
|
34
|
+
{ loader: 'neoforge', ids: [/^net\.neoforged\.moddev$/, /^net\.neoforged\.gradle\.userdev$/, /^net\.neoforged\.gradle$/] },
|
|
35
|
+
{ loader: 'fabric', ids: [/^fabric-loom$/, /^net\.fabricmc\.fabric-loom$/] },
|
|
36
|
+
{ loader: 'forge', ids: [/^net\.minecraftforge\.gradle(\.userdev)?$/, /^forge$/] },
|
|
37
|
+
{ loader: 'paper', ids: [/^paperweight/, /^io\.papermc\.paper/] },
|
|
38
|
+
{ loader: 'spigot', ids: [/^spigot/] },
|
|
39
|
+
{ loader: 'architectury', ids: [/^architectury-plugin$/, /^architectury$/, /^dev\.architectury\.loom$/] },
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
/** 推测默认目标:只在构建脚本读不到 runs 块时使用。 */
|
|
43
|
+
const DEFAULT_TARGETS = {
|
|
44
|
+
fabric: [
|
|
45
|
+
{ block: 'client', kind: 'client' },
|
|
46
|
+
{ block: 'server', kind: 'server' },
|
|
47
|
+
],
|
|
48
|
+
forge: [
|
|
49
|
+
{ block: 'client', kind: 'client' },
|
|
50
|
+
{ block: 'server', kind: 'server' },
|
|
51
|
+
{ block: 'data', kind: 'data' },
|
|
52
|
+
],
|
|
53
|
+
neoforge: [
|
|
54
|
+
{ block: 'client', kind: 'client' },
|
|
55
|
+
{ block: 'server', kind: 'server' },
|
|
56
|
+
{ block: 'data', kind: 'data' },
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 识别一个工程。
|
|
62
|
+
* @param {string} root 工程根目录(绝对路径)
|
|
63
|
+
* @param {{ jdks?: import('./java.js').JdkInfo[], maxModules?: number }} [options]
|
|
64
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
65
|
+
*/
|
|
66
|
+
export async function detectProject(root, options = {}) {
|
|
67
|
+
/** @type {Evidence[]} */
|
|
68
|
+
const evidence = []
|
|
69
|
+
/** @type {string[]} */
|
|
70
|
+
const warnings = []
|
|
71
|
+
/** @type {string[]} */
|
|
72
|
+
const conflicts = []
|
|
73
|
+
|
|
74
|
+
const gradleFile = await pickExisting(root, ['build.gradle.kts', 'build.gradle'])
|
|
75
|
+
const gradleText = gradleFile === undefined ? undefined : await readText(join(root, gradleFile))
|
|
76
|
+
const settingsFile = await pickExisting(root, ['settings.gradle.kts', 'settings.gradle'])
|
|
77
|
+
const settingsText = settingsFile === undefined ? undefined : await readText(join(root, settingsFile))
|
|
78
|
+
const properties = parseProperties(await readText(join(root, 'gradle.properties')))
|
|
79
|
+
const wrapperProperties = parseProperties(await readText(join(root, 'gradle/wrapper/gradle-wrapper.properties')))
|
|
80
|
+
const gradleVersion = parseWrapperVersion(wrapperProperties)
|
|
81
|
+
|
|
82
|
+
const pluginIds = [...parsePluginIds(gradleText ?? ''), ...parsePluginIds(settingsText ?? '')]
|
|
83
|
+
const includes = parseSettingsIncludes(settingsText ?? '')
|
|
84
|
+
|
|
85
|
+
if (gradleFile !== undefined) {
|
|
86
|
+
evidence.push({ file: gradleFile, kind: 'build-script', detail: `Gradle 构建脚本(${gradleFile.endsWith('.kts') ? 'Kotlin DSL' : 'Groovy'})` })
|
|
87
|
+
}
|
|
88
|
+
if (gradleVersion !== undefined) {
|
|
89
|
+
evidence.push({ file: 'gradle/wrapper/gradle-wrapper.properties', kind: 'wrapper', detail: `Gradle ${gradleVersion}` })
|
|
90
|
+
} else if (gradleFile !== undefined) {
|
|
91
|
+
warnings.push('没有 gradle wrapper,需要本机装 Gradle 才能构建')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── 模块展开 ────────────────────────────────────────────────────────
|
|
95
|
+
const modulePaths = includes.length > 0 ? ['.', ...includes.slice(0, options.maxModules ?? 12)] : ['.']
|
|
96
|
+
/** @type {ModuleInfo[]} */
|
|
97
|
+
const modules = []
|
|
98
|
+
/** @type {Awaited<ReturnType<typeof locateMetadata>>} */
|
|
99
|
+
let allMetadata = []
|
|
100
|
+
/** @type {string[]} */
|
|
101
|
+
const modulePluginIds = []
|
|
102
|
+
/** @type {Array<Record<string, string>>} 模块级 gradle.properties,聚合仓库的版本常写在子模块里 */
|
|
103
|
+
const moduleProperties = []
|
|
104
|
+
/** @type {number[]} 模块级 Java toolchain */
|
|
105
|
+
const moduleJavaLevels = []
|
|
106
|
+
|
|
107
|
+
for (const modulePath of modulePaths) {
|
|
108
|
+
const dir = modulePath === '.' ? root : join(root, modulePath)
|
|
109
|
+
const moduleGradleFile = await pickExisting(dir, ['build.gradle.kts', 'build.gradle'])
|
|
110
|
+
const moduleText = moduleGradleFile === undefined ? undefined : await readText(join(dir, moduleGradleFile))
|
|
111
|
+
const moduleIds = parsePluginIds(moduleText ?? '')
|
|
112
|
+
modulePluginIds.push(...moduleIds)
|
|
113
|
+
const runs = parseRuns(moduleText ?? '')
|
|
114
|
+
const metadata = await locateMetadata(dir)
|
|
115
|
+
allMetadata = allMetadata.concat(metadata)
|
|
116
|
+
const platform = detectPlatform(moduleIds, moduleText ?? '')
|
|
117
|
+
moduleProperties.push(parseProperties(await readText(join(dir, 'gradle.properties'))))
|
|
118
|
+
const javaLevel = parseJavaLevel(moduleText ?? '')
|
|
119
|
+
if (javaLevel !== undefined) moduleJavaLevels.push(javaLevel)
|
|
120
|
+
if (metadata.length > 0) {
|
|
121
|
+
evidence.push({
|
|
122
|
+
file: modulePath === '.' ? metadata[0].file : `${modulePath}/${metadata[0].file}`,
|
|
123
|
+
kind: `metadata:${metadata[0].kind}`,
|
|
124
|
+
detail: `${metadata[0].kind} 元数据`,
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
modules.push({ path: modulePath, dir, platform: platform ?? 'unknown', pluginIds: moduleIds, targets: runs.map(toTarget) })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 聚合仓库(如 AnnoCore)把版本写在子模块里:根属性优先,模块属性补齐。
|
|
131
|
+
/** @type {Record<string, string>} */
|
|
132
|
+
const mergedProperties = Object.assign({}, ...moduleProperties.slice().reverse(), properties)
|
|
133
|
+
|
|
134
|
+
// ── loader ─────────────────────────────────────────────────────────
|
|
135
|
+
// 先看根脚本;根脚本没声明时再看子模块(聚合仓库常见:AnnoCore 的 loader 全在子模块里)。
|
|
136
|
+
const allPluginIds = [...new Set([...pluginIds, ...modulePluginIds])]
|
|
137
|
+
/** @type {string | undefined} */
|
|
138
|
+
let loader
|
|
139
|
+
/** @type {string | undefined} */
|
|
140
|
+
let loaderEvidenceId
|
|
141
|
+
for (const rule of LOADER_RULES) {
|
|
142
|
+
const hit = pluginIds.find((id) => rule.ids.some((pattern) => pattern.test(id)))
|
|
143
|
+
if (hit !== undefined) {
|
|
144
|
+
loader = rule.loader
|
|
145
|
+
loaderEvidenceId = hit
|
|
146
|
+
evidence.push({ file: gradleFile ?? 'build.gradle', kind: 'plugin', detail: `插件 ${hit}` })
|
|
147
|
+
break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (loader === undefined) {
|
|
151
|
+
for (const rule of LOADER_RULES) {
|
|
152
|
+
const hit = modulePluginIds.find((id) => rule.ids.some((pattern) => pattern.test(id)))
|
|
153
|
+
if (hit !== undefined) {
|
|
154
|
+
loader = rule.loader
|
|
155
|
+
loaderEvidenceId = hit
|
|
156
|
+
evidence.push({ file: '子模块构建脚本', kind: 'plugin', detail: `插件 ${hit}(只在子模块声明)` })
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 所有命中过的 loader(多平台工程可能同时有 fabric 与 neoforge)
|
|
163
|
+
const loaders = [
|
|
164
|
+
...new Set(
|
|
165
|
+
LOADER_RULES.filter((rule) => allPluginIds.some((id) => rule.ids.some((pattern) => pattern.test(id)))).map(
|
|
166
|
+
(rule) => rule.loader,
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
const metadataKind = primaryMetadata(allMetadata)?.kind
|
|
172
|
+
if (loader === undefined && metadataKind !== undefined) {
|
|
173
|
+
loader = metadataKind === 'bukkit' ? 'spigot' : metadataKind
|
|
174
|
+
loaders.push(loader)
|
|
175
|
+
evidence.push({ file: 'metadata', kind: 'metadata-only', detail: `只有 ${metadataKind} 元数据,没有构建脚本特征` })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 元数据与构建脚本矛盾时记冲突,不静默选一个
|
|
179
|
+
if (loader !== undefined && metadataKind !== undefined) {
|
|
180
|
+
const mapped = metadataKind === 'bukkit' ? 'spigot' : metadataKind
|
|
181
|
+
const compatible = loader === mapped || (loader === 'architectury' && mapped !== 'architectury')
|
|
182
|
+
if (!compatible) {
|
|
183
|
+
conflicts.push(`构建脚本表明 ${loader},元数据却是 ${mapped}`)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── 版本 ────────────────────────────────────────────────────────────
|
|
188
|
+
const minecraftVersion =
|
|
189
|
+
pickProperty(mergedProperties, ['minecraft_version', 'minecraftVersion', 'mc_version', 'mcVersion', 'version_minecraft']) ??
|
|
190
|
+
minecraftFromNeoVersion(pickProperty(mergedProperties, ['neo_version', 'neoVersion'])) ??
|
|
191
|
+
(await versionFromMetadata(allMetadata)) ??
|
|
192
|
+
parseMinecraftVersion(gradleText ?? '', mergedProperties) ??
|
|
193
|
+
pickProperty(mergedProperties, ['parchment_minecraft_version'])
|
|
194
|
+
|
|
195
|
+
const loaderVersion = pickProperty(mergedProperties, [
|
|
196
|
+
'neo_version',
|
|
197
|
+
'neoVersion',
|
|
198
|
+
'loader_version',
|
|
199
|
+
'loaderVersion',
|
|
200
|
+
'forge_version',
|
|
201
|
+
'forgeVersion',
|
|
202
|
+
'fabric_loader_version',
|
|
203
|
+
'fabric_api_version',
|
|
204
|
+
'paper_api_version',
|
|
205
|
+
'spigot_version',
|
|
206
|
+
])
|
|
207
|
+
|
|
208
|
+
// ── 身份 ────────────────────────────────────────────────────────────
|
|
209
|
+
const identity = readIdentity(allMetadata, mergedProperties, root, warnings)
|
|
210
|
+
|
|
211
|
+
// ── 可运行目标 ──────────────────────────────────────────────────────
|
|
212
|
+
const hasDeclaredTargets = modules.some((module) => module.targets.length > 0)
|
|
213
|
+
if (!hasDeclaredTargets && loader !== undefined) {
|
|
214
|
+
applyGuessedTargets(modules, loader)
|
|
215
|
+
if (modules.some((module) => module.targets.length > 0)) {
|
|
216
|
+
warnings.push('构建脚本里没读到 runs 块,目标是按 loader 惯例推测的(未验证)')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const runTargets = modules.flatMap((module) =>
|
|
220
|
+
module.targets.map((target) => ({
|
|
221
|
+
...target,
|
|
222
|
+
module: module.path,
|
|
223
|
+
taskPath: module.path === '.' ? target.task : `:${module.path}:${target.task}`,
|
|
224
|
+
platform: module.platform,
|
|
225
|
+
})),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// ── JDK ────────────────────────────────────────────────────────────
|
|
229
|
+
const toolchain = parseJavaLevel(gradleText ?? '') ?? (moduleJavaLevels.length > 0 ? Math.max(...moduleJavaLevels) : undefined)
|
|
230
|
+
const javaRequired = toolchain ?? requiredJava(/** @type {string | undefined} */ (minecraftVersion))
|
|
231
|
+
const configuredJavaHome = pickProperty(properties, ['org.gradle.java.home'])
|
|
232
|
+
const jdks = options.jdks ?? []
|
|
233
|
+
const picked = pickJdk(jdks, javaRequired, configuredJavaHome)
|
|
234
|
+
if (picked === undefined) {
|
|
235
|
+
warnings.push(`没找到可用的 JDK${javaRequired === undefined ? '' : ` ${javaRequired}`}`)
|
|
236
|
+
} else if (!picked.exact && javaRequired !== undefined) {
|
|
237
|
+
warnings.push(`JDK ${javaRequired} 未找到,退而使用 ${picked.home}`)
|
|
238
|
+
}
|
|
239
|
+
const gradleCheck = gradleSupportsJava(gradleVersion, javaRequired)
|
|
240
|
+
if (!gradleCheck.ok && gradleCheck.reason !== undefined) warnings.push(gradleCheck.reason)
|
|
241
|
+
if (properties['org.gradle.configuration-cache'] === 'true') {
|
|
242
|
+
warnings.push('已开启 org.gradle.configuration-cache,与部分 mod 插件组合可能不兼容')
|
|
243
|
+
}
|
|
244
|
+
if (conflicts.length > 0) warnings.push(`识别到矛盾:${conflicts.join(';')}`)
|
|
245
|
+
|
|
246
|
+
const kind = classify({ modules, loader, loaders, metadataKind })
|
|
247
|
+
const confidence = scoreConfidence({ loader, metadataKind, configuredJavaHome })
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
root,
|
|
251
|
+
name: basename(root),
|
|
252
|
+
kind,
|
|
253
|
+
loader: loader ?? 'unknown',
|
|
254
|
+
loaders,
|
|
255
|
+
loaderEvidence: loaderEvidenceId,
|
|
256
|
+
loaderVersion,
|
|
257
|
+
minecraftVersion,
|
|
258
|
+
modId: identity.modId,
|
|
259
|
+
modName: identity.modName,
|
|
260
|
+
modVersion: identity.modVersion,
|
|
261
|
+
javaVersion: javaRequired,
|
|
262
|
+
java: {
|
|
263
|
+
required: javaRequired,
|
|
264
|
+
toolchain,
|
|
265
|
+
configured: configuredJavaHome,
|
|
266
|
+
selected: picked?.home,
|
|
267
|
+
exact: picked?.exact ?? false,
|
|
268
|
+
available: jdks.map((jdk) => ({ home: jdk.home, major: jdk.major })),
|
|
269
|
+
},
|
|
270
|
+
gradleVersion,
|
|
271
|
+
wrapper: gradleVersion !== undefined,
|
|
272
|
+
modules: modules.map((module) => ({
|
|
273
|
+
path: module.path,
|
|
274
|
+
platform: module.platform,
|
|
275
|
+
targets: module.targets.map((target) => ({
|
|
276
|
+
...target,
|
|
277
|
+
taskPath: module.path === '.' ? target.task : `:${module.path}:${target.task}`,
|
|
278
|
+
})),
|
|
279
|
+
})),
|
|
280
|
+
runTargets,
|
|
281
|
+
evidence,
|
|
282
|
+
conflicts,
|
|
283
|
+
warnings,
|
|
284
|
+
confidence,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @param {string} root
|
|
290
|
+
* @param {string[]} names
|
|
291
|
+
* @returns {Promise<string | undefined>}
|
|
292
|
+
*/
|
|
293
|
+
async function pickExisting(root, names) {
|
|
294
|
+
for (const name of names) {
|
|
295
|
+
try {
|
|
296
|
+
const info = await stat(join(root, name))
|
|
297
|
+
if (info.isFile()) return name
|
|
298
|
+
} catch {
|
|
299
|
+
/* 继续找下一个 */
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return undefined
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @param {string[]} pluginIds
|
|
307
|
+
* @param {string} moduleText
|
|
308
|
+
* @returns {string | undefined} fabric / forge / neoforge / common / undefined
|
|
309
|
+
*/
|
|
310
|
+
function detectPlatform(pluginIds, moduleText) {
|
|
311
|
+
if (pluginIds.some((id) => /fabric-loom/.test(id))) return 'fabric'
|
|
312
|
+
if (pluginIds.some((id) => /neoforged/.test(id))) return 'neoforge'
|
|
313
|
+
if (pluginIds.some((id) => /minecraftforge/.test(id))) return 'forge'
|
|
314
|
+
// Architectury 的平台由 architectury { fabric() } / { forge() } 声明
|
|
315
|
+
if (/\barchitectury\s*\{[^}]*\bfabric\s*\(/.test(moduleText)) return 'fabric'
|
|
316
|
+
if (/\barchitectury\s*\{[^}]*\bforge\s*\(/.test(moduleText)) return 'forge'
|
|
317
|
+
if (/\bmedialib\s*\(|\bcommon\s*\(\s*\)/.test(moduleText)) return 'common'
|
|
318
|
+
return undefined
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @param {{ block: string, kind: string, task: string, label: string }} run
|
|
323
|
+
* @returns {Target}
|
|
324
|
+
*/
|
|
325
|
+
function toTarget(run) {
|
|
326
|
+
return { task: run.task, label: run.label, kind: run.kind, guessed: false }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 读不到 runs 时,按多少给候选:
|
|
331
|
+
* - 单模块工程:给完整的默认集(Client/Server[/Data]),因为只有一个入口;
|
|
332
|
+
* - 多模块工程:每个平台模块只给一个 Client,避免聚合仓库列出十几行噪音。
|
|
333
|
+
* architectury 的 common 模块不猜(它通常没有客户端)。
|
|
334
|
+
* @param {ModuleInfo[]} modules
|
|
335
|
+
* @param {string} loader
|
|
336
|
+
*/
|
|
337
|
+
function applyGuessedTargets(modules, loader) {
|
|
338
|
+
const concrete = modules.filter(
|
|
339
|
+
(module) => module.targets.length === 0 && module.platform !== 'unknown' && module.platform !== 'common',
|
|
340
|
+
)
|
|
341
|
+
if (concrete.length === 0) return
|
|
342
|
+
const singleRoot = concrete.length === 1 && concrete[0].path === '.'
|
|
343
|
+
|
|
344
|
+
for (const module of concrete) {
|
|
345
|
+
const defaults = DEFAULT_TARGETS[/** @type {keyof typeof DEFAULT_TARGETS} */ (module.platform)]
|
|
346
|
+
if (defaults === undefined) continue
|
|
347
|
+
const chosen = singleRoot ? defaults : defaults.filter((entry) => entry.kind === 'client')
|
|
348
|
+
module.targets = chosen.map((entry) => ({
|
|
349
|
+
task: `run${entry.block[0].toUpperCase()}${entry.block.slice(1)}`,
|
|
350
|
+
label: entry.kind === 'gameTestServer' ? 'GameTestServer' : entry.kind[0].toUpperCase() + entry.kind.slice(1),
|
|
351
|
+
kind: entry.kind,
|
|
352
|
+
guessed: true,
|
|
353
|
+
}))
|
|
354
|
+
}
|
|
355
|
+
void loader
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @param {Record<string, string>} properties
|
|
360
|
+
* @param {string[]} keys
|
|
361
|
+
* @returns {string | undefined}
|
|
362
|
+
*/
|
|
363
|
+
function pickProperty(properties, keys) {
|
|
364
|
+
for (const key of keys) {
|
|
365
|
+
const value = properties[key]
|
|
366
|
+
if (value !== undefined && value !== '') return value
|
|
367
|
+
}
|
|
368
|
+
return undefined
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* @param {Awaited<ReturnType<typeof locateMetadata>>} hits
|
|
373
|
+
* @returns {Promise<string | undefined>}
|
|
374
|
+
*/
|
|
375
|
+
async function versionFromMetadata(hits) {
|
|
376
|
+
const primary = primaryMetadata(hits)
|
|
377
|
+
if (primary === undefined) return undefined
|
|
378
|
+
const ranges = dependencyRanges(primary)
|
|
379
|
+
const range = ranges.minecraft
|
|
380
|
+
if (range !== undefined) {
|
|
381
|
+
const match = /(\d+\.\d+(?:\.\d+)?)/.exec(range)
|
|
382
|
+
if (match) return match[1]
|
|
383
|
+
}
|
|
384
|
+
if (primary.kind === 'mcmod' && typeof primary.data.mcversion === 'string') {
|
|
385
|
+
return primary.data.mcversion
|
|
386
|
+
}
|
|
387
|
+
return undefined
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* @param {Awaited<ReturnType<typeof locateMetadata>>} hits
|
|
392
|
+
* @param {Record<string, string>} properties
|
|
393
|
+
* @param {string} root
|
|
394
|
+
* @param {string[]} warnings
|
|
395
|
+
*/
|
|
396
|
+
function readIdentity(hits, properties, root, warnings) {
|
|
397
|
+
const primary = primaryMetadata(hits)
|
|
398
|
+
const data = primary?.data ?? {}
|
|
399
|
+
/** @type {string[]} */
|
|
400
|
+
const unresolved = []
|
|
401
|
+
|
|
402
|
+
const raw = {
|
|
403
|
+
modId: asString(data.id) ?? asString(data.modId) ?? asString(data.modid) ?? asString(data.name),
|
|
404
|
+
modName: asString(data.name) ?? asString(data.displayName),
|
|
405
|
+
modVersion: asString(data.version),
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const resolve = (value) => {
|
|
409
|
+
if (value === undefined) return undefined
|
|
410
|
+
const result = resolvePlaceholders(value, properties)
|
|
411
|
+
if (result.unresolved.length > 0) unresolved.push(...result.unresolved)
|
|
412
|
+
return result.value
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const modId = resolve(raw.modId) ?? properties.mod_id ?? properties.modid
|
|
416
|
+
const modName = resolve(raw.modName) ?? properties.mod_name ?? modId ?? basename(root)
|
|
417
|
+
const modVersion = resolve(raw.modVersion) ?? properties.mod_version ?? properties.version
|
|
418
|
+
|
|
419
|
+
if (unresolved.length > 0) {
|
|
420
|
+
warnings.push(`元数据里有未解析的占位符:${[...new Set(unresolved)].join(', ')}`)
|
|
421
|
+
}
|
|
422
|
+
return { modId, modName, modVersion }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {unknown} value
|
|
427
|
+
* @returns {string | undefined}
|
|
428
|
+
*/
|
|
429
|
+
function asString(value) {
|
|
430
|
+
return typeof value === 'string' && value !== '' ? value : undefined
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 工程种类。判 multi-loader 要求"至少认出一种 loader"——什么都没认出来时
|
|
435
|
+
* 只能说 unknown,不能因为模块平台标签不一致就升级成多平台。
|
|
436
|
+
* @param {{ modules: ModuleInfo[], loader?: string, loaders: string[], metadataKind?: string }} input
|
|
437
|
+
* @returns {string}
|
|
438
|
+
*/
|
|
439
|
+
function classify(input) {
|
|
440
|
+
if (input.metadataKind === 'bukkit' || input.metadataKind === 'paper') return 'server-plugin'
|
|
441
|
+
if (input.loader === undefined) return 'unknown'
|
|
442
|
+
if (input.loader === 'architectury') return 'multi-loader'
|
|
443
|
+
const concrete = input.loaders.filter((loader) => loader !== 'architectury')
|
|
444
|
+
if (concrete.length > 1) return 'multi-loader'
|
|
445
|
+
const platforms = new Set(
|
|
446
|
+
input.modules.map((module) => module.platform).filter((platform) => platform !== 'unknown' && platform !== 'common'),
|
|
447
|
+
)
|
|
448
|
+
if (platforms.size > 1) return 'multi-loader'
|
|
449
|
+
return 'mod'
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* @param {{ loader?: string, metadataKind?: string, configuredJavaHome?: string }} input
|
|
454
|
+
* @returns {number}
|
|
455
|
+
*/
|
|
456
|
+
function scoreConfidence(input) {
|
|
457
|
+
if (input.loader === undefined) return 0.2
|
|
458
|
+
if (input.metadataKind !== undefined) return 0.95
|
|
459
|
+
return input.loader === 'architectury' ? 0.7 : 0.8
|
|
460
|
+
}
|