@adhisang/minecraft-modding-mcp 7.0.0 → 7.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +4 -3
  3. package/dist/access-transformer-parser.d.ts +8 -0
  4. package/dist/access-transformer-parser.js +8 -1
  5. package/dist/access-widener-parser.d.ts +17 -0
  6. package/dist/access-widener-parser.js +12 -1
  7. package/dist/cache-registry.js +19 -6
  8. package/dist/entry-tools/analyze-mod-service.d.ts +2 -2
  9. package/dist/entry-tools/analyze-symbol-service.d.ts +2 -2
  10. package/dist/entry-tools/batch-class-members-service.d.ts +3 -2
  11. package/dist/entry-tools/batch-class-members-service.js +20 -6
  12. package/dist/entry-tools/batch-class-source-service.d.ts +3 -2
  13. package/dist/entry-tools/batch-class-source-service.js +10 -0
  14. package/dist/entry-tools/compare-minecraft-service.d.ts +27 -4
  15. package/dist/entry-tools/compare-minecraft-service.js +65 -4
  16. package/dist/entry-tools/entry-tool-schema.d.ts +2 -2
  17. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +15 -9
  18. package/dist/entry-tools/inspect-minecraft-service.d.ts +2 -2
  19. package/dist/entry-tools/manage-cache-service.d.ts +2 -2
  20. package/dist/entry-tools/manage-cache-service.js +10 -14
  21. package/dist/entry-tools/validate-project/cases/access-transformer.js +22 -3
  22. package/dist/entry-tools/validate-project/cases/access-widener.js +31 -4
  23. package/dist/entry-tools/validate-project/cases/mixin.js +11 -3
  24. package/dist/entry-tools/validate-project/cases/project-summary.js +94 -16
  25. package/dist/entry-tools/validate-project-service.d.ts +2 -2
  26. package/dist/entry-tools/verify-mixin-target-service.js +19 -8
  27. package/dist/index.js +38 -15
  28. package/dist/java-process.d.ts +1 -0
  29. package/dist/java-process.js +14 -0
  30. package/dist/mapping/lookup.js +16 -1
  31. package/dist/mapping-service.d.ts +14 -0
  32. package/dist/mapping-service.js +35 -15
  33. package/dist/minecraft-explorer-service.js +70 -8
  34. package/dist/mixin/access-validators.js +38 -2
  35. package/dist/mixin/annotation-validators.js +137 -43
  36. package/dist/mixin/parsed-validator.js +21 -7
  37. package/dist/mixin-parser.d.ts +52 -0
  38. package/dist/mixin-parser.js +709 -130
  39. package/dist/mod-decompile-service.js +11 -1
  40. package/dist/mod-remap-service.js +6 -6
  41. package/dist/nbt/java-nbt-codec.js +7 -1
  42. package/dist/source/access-validate.js +10 -0
  43. package/dist/source/artifact-resolver.d.ts +27 -3
  44. package/dist/source/artifact-resolver.js +235 -30
  45. package/dist/source/class-source/members-builder.d.ts +7 -0
  46. package/dist/source/class-source/members-builder.js +4 -1
  47. package/dist/source/class-source.d.ts +9 -2
  48. package/dist/source/class-source.js +186 -27
  49. package/dist/source/indexer.js +69 -1
  50. package/dist/source/lifecycle/mapping-helpers.d.ts +20 -1
  51. package/dist/source/lifecycle/mapping-helpers.js +29 -3
  52. package/dist/source/lifecycle/runtime-check.d.ts +25 -0
  53. package/dist/source/lifecycle/runtime-check.js +68 -39
  54. package/dist/source/nested-jars.d.ts +15 -1
  55. package/dist/source/nested-jars.js +14 -5
  56. package/dist/source/search.d.ts +10 -2
  57. package/dist/source/search.js +60 -13
  58. package/dist/source/symbol-resolver.js +88 -0
  59. package/dist/source/validate-mixin/pipeline/mapping-health.js +20 -1
  60. package/dist/source/validate-mixin/pipeline/target-lookup.js +16 -7
  61. package/dist/source/validate-mixin.d.ts +5 -0
  62. package/dist/source/validate-mixin.js +136 -21
  63. package/dist/source/workspace-target.js +75 -7
  64. package/dist/source-jar-reader.d.ts +48 -1
  65. package/dist/source-jar-reader.js +93 -3
  66. package/dist/source-resolver.d.ts +7 -0
  67. package/dist/source-resolver.js +22 -14
  68. package/dist/source-service.d.ts +5 -0
  69. package/dist/source-service.js +7 -0
  70. package/dist/stdio-supervisor.d.ts +35 -1
  71. package/dist/stdio-supervisor.js +77 -2
  72. package/dist/storage/db.d.ts +62 -2
  73. package/dist/storage/db.js +181 -20
  74. package/dist/storage/files-repo.d.ts +7 -0
  75. package/dist/storage/files-repo.js +17 -4
  76. package/dist/storage/sqlite.d.ts +31 -1
  77. package/dist/storage/sqlite.js +125 -16
  78. package/dist/tool-contract-manifest.js +2 -2
  79. package/dist/tool-execution-gate.js +2 -1
  80. package/dist/tool-guidance.js +4 -1
  81. package/dist/tool-schemas.d.ts +64 -52
  82. package/dist/tool-schemas.js +9 -7
  83. package/dist/types.d.ts +9 -0
  84. package/dist/v1-parity-schemas.js +36 -2
  85. package/dist/version-diff-service.d.ts +23 -0
  86. package/dist/version-diff-service.js +101 -0
  87. package/dist/version-service.d.ts +14 -0
  88. package/dist/version-service.js +52 -3
  89. package/dist/workspace-context-cache.d.ts +25 -0
  90. package/dist/workspace-context-cache.js +52 -2
  91. package/dist/workspace-mapping-service.d.ts +8 -0
  92. package/dist/workspace-mapping-service.js +151 -21
  93. package/docs/README-ja.md +3 -1
  94. package/docs/tool-reference.md +69 -22
  95. package/package.json +1 -1
@@ -135,7 +135,17 @@ export class ModDecompileService {
135
135
  const outPath = isAbsolute(input.outputFile)
136
136
  ? input.outputFile
137
137
  : resolvePath(input.outputFile);
138
- await writeFile(outPath, content, "utf8");
138
+ try {
139
+ await writeFile(outPath, content, "utf8");
140
+ }
141
+ catch (writeError) {
142
+ const errno = writeError?.code;
143
+ throw createError({
144
+ code: ERROR_CODES.INVALID_INPUT,
145
+ message: `outputFile "${outPath}" could not be written (${errno ?? "unknown error"}).`,
146
+ details: { field: "outputFile", value: outPath, osErrorCode: errno }
147
+ });
148
+ }
139
149
  outputFilePath = outPath;
140
150
  content = `[Written to ${outPath}]`;
141
151
  }
@@ -149,13 +149,13 @@ export async function remapModJar(input, config, deps = {}) {
149
149
  mkdirSync(cacheDir, { recursive: true });
150
150
  const cachedOutput = join(cacheDir, `${cacheKey}.jar`);
151
151
  if (!input.forceRemap && existsSync(cachedOutput)) {
152
- const cacheHitOutputJar = input.outputJar
153
- ? outputJar
154
- : cachedOutput;
155
- copyJarToDestination(cachedOutput, cacheHitOutputJar);
156
- log("info", "remap.cache-hit", { inputJar: normalizedInput, outputJar: cacheHitOutputJar });
152
+ // Always copy to the same `outputJar` location a cold run would use (the
153
+ // caller-supplied path, or resolveOutputJarPath's "next to input" default),
154
+ // so a cache hit and a cold run return the same path convention.
155
+ copyJarToDestination(cachedOutput, outputJar);
156
+ log("info", "remap.cache-hit", { inputJar: normalizedInput, outputJar });
157
157
  return {
158
- outputJar: cacheHitOutputJar,
158
+ outputJar,
159
159
  mcVersion,
160
160
  fromMapping: fromNamespace,
161
161
  targetMapping: input.targetMapping,
@@ -480,7 +480,13 @@ export function decodeJavaNbt(buffer) {
480
480
  throw error;
481
481
  }
482
482
  }
483
- throw parseError("Failed to decode Java NBT payload.");
483
+ // Unlike every other parseError() call site in this module, nothing here
484
+ // has an offset to report - do not inherit PARSE_NEXT_ACTION's "at the
485
+ // reported offset" promise.
486
+ throw parseError("Failed to decode Java NBT payload.", {
487
+ nextAction: "The payload is not a well-formed Java NBT stream. Check that nbtBase64 holds the whole " +
488
+ 'file and that compression matches how it was written (use compression "auto" to detect gzip).'
489
+ });
484
490
  }
485
491
  }
486
492
  export function encodeJavaNbt(document) {
@@ -80,6 +80,16 @@ export async function validateAccessWidener(svc, input) {
80
80
  if (!content.trim()) {
81
81
  throw createError({ code: ERROR_CODES.INVALID_INPUT, message: "content must be non-empty." });
82
82
  }
83
+ // Fabric Loader also reads the classTweaker format; it is out of scope here rather than an
84
+ // accessWidener file with a bad header, so it gets no verdict.
85
+ const header = content.split(/\r?\n/).map((line) => line.trim()).find((line) => line && !line.startsWith("#"));
86
+ if (header && /^classTweaker(\s|$)/.test(header)) {
87
+ throw createError({
88
+ code: ERROR_CODES.INVALID_INPUT,
89
+ message: `The classTweaker format is not supported (header "${header}"); only accessWidener v1/v2 files can be validated.`,
90
+ details: { unsupportedFormat: "classTweaker", header }
91
+ });
92
+ }
83
93
  const warnings = [];
84
94
  const parsed = parseAccessWidener(content);
85
95
  const headerNamespaceRaw = normalizeOptionalString(parsed.namespace);
@@ -1,3 +1,4 @@
1
+ import { type JavaSourceScan } from "../source-jar-reader.js";
1
2
  import type { SourceService } from "../source-service.js";
2
3
  import type { ArtifactContentsSummary, ProbeMinecraftArtifactInput, ProbeMinecraftArtifactOutput, ResolveArtifactInput, ResolveArtifactOutput } from "../source-service.js";
3
4
  import type { AccessTransformerNamespace, ArtifactProvenance, ArtifactScope, ArtifactTargetKind, ResolvedSourceArtifact, RuntimeLoader, RuntimeValidationProvenance, SourceMapping } from "../types.js";
@@ -139,10 +140,31 @@ export declare function hasLoaderRuntimeVersionToken(path: string, mcVersion: st
139
140
  /**
140
141
  * Minecraft version a runtime jar path carries, or undefined when the path
141
142
  * names none. Loom lays its cache out as
142
- * `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first `1.x[.y]` token
143
- * of the path is the version the jar was built for.
143
+ * `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first token of the
144
+ * tool-written part of the path ({@link runtimeJarLayoutTail}; the whole path
145
+ * when it enters no layout root) that names a Minecraft version is the version the jar was built for — in
146
+ * either id family, and a pre-release names itself and not the release it
147
+ * precedes. Reading only `1.x[.y]` left every 26.x jar versionless, so no
148
+ * approximation was ever reported for one.
149
+ *
150
+ * A legacy `1.x` token is self-identifying; a 26.1+ token shares its shape with
151
+ * every loader's own version number, so it additionally has to sit where
152
+ * {@link tokenPositionNamesMinecraft} says Minecraft's version sits.
144
153
  */
145
154
  export declare function inferRuntimeJarMinecraftVersion(path: string): string | undefined;
155
+ /**
156
+ * The Minecraft version a `kind: "jar"` target PROVES it is the unobfuscated
157
+ * runtime of, or undefined. The file path is never evidence: Loom and other
158
+ * tools lay jars out in ways that path heuristics have misread before.
159
+ *
160
+ * All three must hold: the walk met no `.java` entry, the jar ships
161
+ * `net/minecraft/SharedConstants.class`, and its root `version.json` parses with a
162
+ * string `id` that `isUnobfuscatedVersion` accepts. The id is put to that test
163
+ * only once the other two hold, so it is the id a Minecraft `version.json`
164
+ * declares, never a library's own release number. A Loom-mapped 1.x jar carries
165
+ * both signals too; its `1.x` id is what keeps it out. The path is never read.
166
+ */
167
+ export declare function provenUnobfuscatedRuntimeJarVersion(scan: JavaSourceScan | undefined): string | undefined;
146
168
  /**
147
169
  * Loader a runtime jar belongs to, read from the tool-written part of its path.
148
170
  *
@@ -163,7 +185,9 @@ export declare function inferRuntimeJarLoader(path: string): RuntimeLoader;
163
185
  * `version` becomes the version the SERVED jar carries; the caller's original
164
186
  * request is preserved under `requestedVersion` and flagged. A served loader
165
187
  * that contradicts a KNOWN expected loader is flagged too — never silently
166
- * dropped.
188
+ * dropped. When the jar's path names no version at all, the request stands as
189
+ * the answer and nothing is flagged: there is no served version to contradict
190
+ * it with.
167
191
  */
168
192
  export declare function describeServedRuntimeJar(input: {
169
193
  jarPath: string;
@@ -170,16 +170,30 @@ export function looksLikeMinecraftArtifactPath(path) {
170
170
  function looksLikeMinecraftSourceArtifact(path, hasMinecraftNamespace) {
171
171
  return hasMinecraftNamespace || looksLikeMinecraftArtifactPath(path);
172
172
  }
173
+ /**
174
+ * What a Minecraft version id may be FOLLOWED by and still be the same id:
175
+ * nothing. A pre-release suffix extends the id rather than decorating it —
176
+ * `26.1-pre-1` is its own version and not `26.1` — so the hyphen forms Mojang
177
+ * publishes (`-pre1`, `-rc1`, `-pre-6`, `-rc-2`, `-snapshot-6`) have to close
178
+ * the token as firmly as a trailing digit does. Any OTHER hyphen is ordinary
179
+ * path decoration (`minecraft-1.21.1-merged.jar`) and must keep matching.
180
+ *
181
+ * The suffix grammar is `isUnobfuscatedVersion`'s in version-service.ts; the
182
+ * two are read together whenever either changes.
183
+ */
184
+ const VERSION_ID_SUFFIX_LOOKAHEAD = "-(?:pre|rc)[0-9]|-(?:pre|rc|snapshot)-[0-9]";
173
185
  export function hasExactVersionToken(path, version) {
174
186
  const normalizedPath = normalizePathStyle(path).toLowerCase();
175
187
  const normalizedVersion = version.trim().toLowerCase();
176
188
  if (!normalizedVersion) {
177
189
  return false;
178
190
  }
179
- // Avoid prefix false-positives like "1.21.1" matching "1.21.10".
180
- const cached = VERSION_TOKEN_REGEX_CACHE.get(normalizedVersion);
191
+ // Avoid prefix false-positives like "1.21.1" matching "1.21.10", and
192
+ // pre-release false-positives like "26.1" matching "26.1-pre-1".
193
+ const cacheKey = normalizedVersion;
194
+ const cached = VERSION_TOKEN_REGEX_CACHE.get(cacheKey);
181
195
  const pattern = cached
182
- ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, normalizedVersion, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}(?![0-9a-z]|\\.[0-9])`, "i"));
196
+ ?? rememberCachedRegex(VERSION_TOKEN_REGEX_CACHE, cacheKey, new RegExp(`(^|[^0-9a-z])${escapeRegexLiteral(normalizedVersion)}(?![0-9a-z]|\\.[0-9]|${VERSION_ID_SUFFIX_LOOKAHEAD})`, "i"));
183
197
  return pattern.test(normalizedPath);
184
198
  }
185
199
  /**
@@ -209,15 +223,119 @@ export function hasLoaderRuntimeVersionToken(path, mcVersion) {
209
223
  ?? rememberCachedRegex(LOADER_VERSION_TOKEN_REGEX_CACHE, cacheKey, new RegExp(`(^|[^0-9a-z.])${minor}\\.${patch}\\.\\d`, "i"));
210
224
  return pattern.test(normalizedPath);
211
225
  }
212
- const RUNTIME_JAR_VERSION_REGEX = /(?:^|[^0-9.])(1\.\d+(?:\.\d+)?)(?![0-9.])/;
226
+ /**
227
+ * Every version-SHAPED token a path carries, in both id families: `<a>.<b>[.<c>]`
228
+ * with an optional pre-release suffix, and the `YYwNNa` snapshot form. Shape
229
+ * only — `inferRuntimeJarMinecraftVersion` decides which of them is Minecraft's.
230
+ *
231
+ * The alternatives are greedy, so the LONGEST token at a position is the one
232
+ * offered, suffix included. What closes it is another version component — a
233
+ * digit, or a dot that starts one — and nothing else: a dot that starts a file
234
+ * extension ends the token instead. Refusing every following dot made
235
+ * `minecraft-merged-26.1.jar` name no version and made
236
+ * `minecraft-merged-26.2-pre-6.jar` backtrack off its own suffix down to
237
+ * "26.2", the release that jar is not.
238
+ */
239
+ const RUNTIME_JAR_VERSION_TOKEN_REGEX = /(?:^|[^0-9.])(\d+\.\d+(?:\.\d+)?(?:-(?:pre|rc)\d+|-(?:pre|rc|snapshot)-\d+)?|\d{2}w\d{2}[a-z])(?![0-9]|\.[0-9])/g;
240
+ /** The legacy obfuscated line, `1.21` / `1.21.10` / `1.14.4-pre7`. */
241
+ const LEGACY_MINECRAFT_VERSION_REGEX = /^1\.\d+(?:\.\d+)?(?:-(?:pre|rc)\d+|-(?:pre|rc|snapshot)-\d+)?$/;
242
+ /** Loom's cache root: the directory whose immediate children are Minecraft versions. */
243
+ const LOOM_CACHE_ROOT_SEGMENT_REGEX = /^(?:fabric-loom|loom-cache)$/;
244
+ /**
245
+ * Does the path POSITION a 26.1+ token sits at vouch for it being Minecraft's
246
+ * version?
247
+ *
248
+ * The new-format grammar is only a shape: `isUnobfuscatedVersion` accepts any
249
+ * id from year 26 up, and Forge's own versions have long since passed it
250
+ * (`forge-47.3.0-srg.jar`), so the token needs the same kind of corroboration
251
+ * the loader-version exclusions elsewhere in this file rely on. Three positions
252
+ * carry it, and they are the layouts the discovery helpers already walk: the
253
+ * artifact itself names Minecraft (`minecraft-merged-26.1.jar`), the entry
254
+ * directly inside the version directory does (neoformruntime's
255
+ * `<version>/minecraft-joined.jar`), or the version directory is a child of
256
+ * Loom's cache root (`<...>/fabric-loom/<mcVersion>/`).
257
+ *
258
+ * Only the token's own neighbourhood counts, never an ancestor — for the reason
259
+ * `runtimeJarLayoutRelativePath` exists: a checkout under `~/dev/minecraft/`
260
+ * names the user's directory, not the jar.
261
+ */
262
+ function tokenPositionNamesMinecraft(segments, index) {
263
+ return ((segments[index]?.includes("minecraft") ?? false) ||
264
+ (segments[index + 1]?.includes("minecraft") ?? false) ||
265
+ LOOM_CACHE_ROOT_SEGMENT_REGEX.test(segments[index - 1] ?? ""));
266
+ }
213
267
  /**
214
268
  * Minecraft version a runtime jar path carries, or undefined when the path
215
269
  * names none. Loom lays its cache out as
216
- * `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first `1.x[.y]` token
217
- * of the path is the version the jar was built for.
270
+ * `<gradle>/caches/fabric-loom/<mcVersion>/...`, so the first token of the
271
+ * tool-written part of the path ({@link runtimeJarLayoutTail}; the whole path
272
+ * when it enters no layout root) that names a Minecraft version is the version the jar was built for — in
273
+ * either id family, and a pre-release names itself and not the release it
274
+ * precedes. Reading only `1.x[.y]` left every 26.x jar versionless, so no
275
+ * approximation was ever reported for one.
276
+ *
277
+ * A legacy `1.x` token is self-identifying; a 26.1+ token shares its shape with
278
+ * every loader's own version number, so it additionally has to sit where
279
+ * {@link tokenPositionNamesMinecraft} says Minecraft's version sits.
218
280
  */
219
281
  export function inferRuntimeJarMinecraftVersion(path) {
220
- return RUNTIME_JAR_VERSION_REGEX.exec(normalizePathStyle(path))?.[1];
282
+ // A path that enters no layout root, such as a custom Gradle home's `loom-cache`, is read whole.
283
+ const normalizedPath = runtimeJarLayoutTail(path) ?? normalizePathStyle(path).toLowerCase();
284
+ const segments = normalizedPath.split("/");
285
+ for (const match of normalizedPath.matchAll(RUNTIME_JAR_VERSION_TOKEN_REGEX)) {
286
+ const token = match[1];
287
+ if (!token) {
288
+ continue;
289
+ }
290
+ if (LEGACY_MINECRAFT_VERSION_REGEX.test(token)) {
291
+ return token;
292
+ }
293
+ if (!isUnobfuscatedVersion(token)) {
294
+ continue;
295
+ }
296
+ // `match[0]` carries the leading boundary character, so the token starts at
297
+ // its end minus the token's own length.
298
+ const tokenStart = (match.index ?? 0) + match[0].length - token.length;
299
+ const segmentIndex = normalizedPath.slice(0, tokenStart).split("/").length - 1;
300
+ if (tokenPositionNamesMinecraft(segments, segmentIndex)) {
301
+ return token;
302
+ }
303
+ }
304
+ return undefined;
305
+ }
306
+ /**
307
+ * The Minecraft version a `kind: "jar"` target PROVES it is the unobfuscated
308
+ * runtime of, or undefined. The file path is never evidence: Loom and other
309
+ * tools lay jars out in ways that path heuristics have misread before.
310
+ *
311
+ * All three must hold: the walk met no `.java` entry, the jar ships
312
+ * `net/minecraft/SharedConstants.class`, and its root `version.json` parses with a
313
+ * string `id` that `isUnobfuscatedVersion` accepts. The id is put to that test
314
+ * only once the other two hold, so it is the id a Minecraft `version.json`
315
+ * declares, never a library's own release number. A Loom-mapped 1.x jar carries
316
+ * both signals too; its `1.x` id is what keeps it out. The path is never read.
317
+ */
318
+ export function provenUnobfuscatedRuntimeJarVersion(scan) {
319
+ const signals = scan?.minecraftRuntimeSignals;
320
+ if (!scan || scan.hasJavaSources || !signals?.hasSharedConstantsClass || signals.versionJsonText === undefined) {
321
+ return undefined;
322
+ }
323
+ let parsed;
324
+ try {
325
+ parsed = JSON.parse(signals.versionJsonText);
326
+ }
327
+ catch {
328
+ return undefined;
329
+ }
330
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
331
+ return undefined;
332
+ }
333
+ const id = parsed.id;
334
+ if (typeof id !== "string") {
335
+ return undefined;
336
+ }
337
+ const version = id.trim();
338
+ return isUnobfuscatedVersion(version) ? version : undefined;
221
339
  }
222
340
  /**
223
341
  * Segment that opens a directory tree a BUILD TOOL wrote: a dot-directory
@@ -237,6 +355,10 @@ const RUNTIME_JAR_LAYOUT_ROOT_RE = /^(?:\.[^/]*|caches|build)$/;
237
355
  * loader for an artifact that names none.
238
356
  */
239
357
  function runtimeJarLayoutRelativePath(path) {
358
+ return runtimeJarLayoutTail(path) ?? normalizePathStyle(path).toLowerCase().split("/").filter(Boolean).at(-1) ?? "";
359
+ }
360
+ /** Everything below the deepest layout root, or undefined when the path enters none. */
361
+ function runtimeJarLayoutTail(path) {
240
362
  const segments = normalizePathStyle(path)
241
363
  .toLowerCase()
242
364
  .split("/")
@@ -246,7 +368,7 @@ function runtimeJarLayoutRelativePath(path) {
246
368
  return segments.slice(index + 1).join("/");
247
369
  }
248
370
  }
249
- return segments[segments.length - 1] ?? "";
371
+ return undefined;
250
372
  }
251
373
  /**
252
374
  * Loader a runtime jar belongs to, read from the tool-written part of its path.
@@ -286,7 +408,9 @@ export function inferRuntimeJarLoader(path) {
286
408
  * `version` becomes the version the SERVED jar carries; the caller's original
287
409
  * request is preserved under `requestedVersion` and flagged. A served loader
288
410
  * that contradicts a KNOWN expected loader is flagged too — never silently
289
- * dropped.
411
+ * dropped. When the jar's path names no version at all, the request stands as
412
+ * the answer and nothing is flagged: there is no served version to contradict
413
+ * it with.
290
414
  */
291
415
  export function describeServedRuntimeJar(input) {
292
416
  const notes = [];
@@ -378,7 +502,8 @@ function buildProvenance(input) {
378
502
  version: input.resolved.version,
379
503
  repoUrl: input.resolved.repoUrl
380
504
  },
381
- transformChain: [...input.transformChain]
505
+ transformChain: [...input.transformChain],
506
+ ...(input.unobfuscatedRuntime ? { unobfuscatedRuntime: true } : {})
382
507
  };
383
508
  if (!provenance.resolvedAt || !provenance.target.kind || !provenance.target.value) {
384
509
  throw createError({
@@ -492,8 +617,9 @@ export async function probeMinecraftArtifact(svc, input) {
492
617
  let value = input.target.value.trim();
493
618
  const warnings = [];
494
619
  const requestedMapping = normalizeMapping(input.mapping);
495
- if (input.preferProjectVersion && input.projectPath) {
496
- const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(input.projectPath);
620
+ const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
621
+ if (input.preferProjectVersion && normalizedProjectPath) {
622
+ const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
497
623
  if (detected && detected !== value) {
498
624
  warnings.push(`Overriding version "${value}" with project version "${detected}" from gradle.properties.`);
499
625
  }
@@ -545,7 +671,7 @@ export async function probeMinecraftArtifact(svc, input) {
545
671
  }
546
672
  const versionSourceDiscovery = await svc.discoverVersionSourceJar({
547
673
  version: resolvedVersion,
548
- projectPath: input.projectPath,
674
+ projectPath: normalizedProjectPath,
549
675
  gradleUserHome: input.gradleUserHome
550
676
  });
551
677
  if (!versionSourceDiscovery.selectedSourceJarPath) {
@@ -1093,10 +1219,11 @@ export async function resolveVersionContext(svc, input) {
1093
1219
  if (inferredVersion) {
1094
1220
  return inferredVersion;
1095
1221
  }
1096
- if (!input.preferProjectVersion || !input.projectPath) {
1222
+ const normalizedProjectPath = normalizeOptionalProjectPath(input.projectPath);
1223
+ if (!input.preferProjectVersion || !normalizedProjectPath) {
1097
1224
  return undefined;
1098
1225
  }
1099
- const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(input.projectPath);
1226
+ const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(normalizedProjectPath);
1100
1227
  if (detected) {
1101
1228
  input.warnings.push(`Using project version "${detected}" from gradle.properties because the artifact metadata did not include a version.`);
1102
1229
  }
@@ -1233,12 +1360,19 @@ export async function buildMappingFallbackSuggestedCall(svc, args) {
1233
1360
  if (process.env.WORKSPACE_FALLBACK_LEGACY === "1") {
1234
1361
  return buildLegacyMappingFallback({ kind, value, scope, isVanillaMojang, projectPath: input.projectPath });
1235
1362
  }
1236
- const projectPath = input.projectPath?.trim();
1363
+ const projectPath = normalizeOptionalProjectPath(input.projectPath);
1237
1364
  if (!projectPath) {
1238
1365
  return buildLegacyMappingFallback({ kind, value, scope, isVanillaMojang, projectPath: undefined });
1239
1366
  }
1240
1367
  if (kind !== "version") {
1241
- return buildLegacyMappingFallback({ kind, value, scope, isVanillaMojang, projectPath });
1368
+ return buildLegacyMappingFallback({
1369
+ kind,
1370
+ value,
1371
+ scope,
1372
+ isVanillaMojang,
1373
+ projectPath,
1374
+ projectRuntimeUnobfuscated: isVanillaMojang && (await projectNamesUnobfuscatedMinecraft(svc, projectPath))
1375
+ });
1242
1376
  }
1243
1377
  const cached = svc.workspaceContextCache.read(projectPath);
1244
1378
  if (cached &&
@@ -1299,9 +1433,28 @@ export async function buildMappingFallbackSuggestedCall(svc, args) {
1299
1433
  }
1300
1434
  return buildLegacyMappingFallback({ kind, value, scope, isVanillaMojang, projectPath });
1301
1435
  }
1436
+ /**
1437
+ * Whether the project's gradle.properties names a Minecraft 26.1+ release. The
1438
+ * property is a Minecraft version by name (`minecraft_version` and its aliases), so
1439
+ * it may be put to `isUnobfuscatedVersion`. Unreadable or absent answers false.
1440
+ */
1441
+ async function projectNamesUnobfuscatedMinecraft(svc, projectPath) {
1442
+ try {
1443
+ const version = await svc.workspaceMappingService.detectProjectMinecraftVersion(projectPath);
1444
+ return version !== undefined && isUnobfuscatedVersion(version);
1445
+ }
1446
+ catch {
1447
+ return false;
1448
+ }
1449
+ }
1302
1450
  function buildLegacyMappingFallback(args) {
1303
1451
  const { kind, value, scope, isVanillaMojang, projectPath } = args;
1304
- if (isVanillaMojang && projectPath) {
1452
+ // scope=merged reaches Loom source discovery only for a version target. For a
1453
+ // jar or coordinate in a 26.1+ project, re-suggesting mojang+merged on the same
1454
+ // target repeats the request that was just refused, so that case takes the
1455
+ // generic retry below instead.
1456
+ const mergedRetryWouldRepeat = kind !== "version" && args.projectRuntimeUnobfuscated === true;
1457
+ if (isVanillaMojang && projectPath && !mergedRetryWouldRepeat) {
1305
1458
  return {
1306
1459
  ...buildSuggestedCall({
1307
1460
  tool: "resolve-artifact",
@@ -1311,7 +1464,7 @@ function buildLegacyMappingFallback(args) {
1311
1464
  "Retry with scope=merged to allow source-jar resolution from the project cache."
1312
1465
  };
1313
1466
  }
1314
- if (isVanillaMojang) {
1467
+ if (isVanillaMojang && !projectPath) {
1315
1468
  return {
1316
1469
  ...buildSuggestedCall({
1317
1470
  tool: "resolve-artifact",
@@ -1326,7 +1479,14 @@ function buildLegacyMappingFallback(args) {
1326
1479
  tool: "resolve-artifact",
1327
1480
  params: buildResolveArtifactParams({ kind, value }, { mapping: "obfuscated", ...(scope ? { scope } : {}) })
1328
1481
  }),
1329
- nextAction: "Retry with mapping=obfuscated to use the runtime obfuscated namespace."
1482
+ // A jar reaches here only when it was NOT proven to be a Minecraft runtime jar,
1483
+ // so its version is unknown and "obfuscated" must not be described as
1484
+ // obfuscated names: on 26.1+ the as-shipped names are Mojang names. Version and
1485
+ // coordinate refusals keep their established wording.
1486
+ nextAction: kind === "jar"
1487
+ ? "Retry with mapping=obfuscated to read this jar's names as shipped (already Mojang names on Minecraft 26.1+). " +
1488
+ "For a vanilla Minecraft release, use target kind \"version\" instead."
1489
+ : "Retry with mapping=obfuscated to use the runtime obfuscated namespace."
1330
1490
  };
1331
1491
  }
1332
1492
  export async function resolveArtifact(svc, input) {
@@ -1360,8 +1520,15 @@ export async function resolveArtifact(svc, input) {
1360
1520
  const mapping = normalizeMapping(input.mapping);
1361
1521
  const scope = input.scope;
1362
1522
  const warnings = [...synthesisWarnings];
1363
- if (input.preferProjectVersion && input.projectPath && kind === "version") {
1364
- const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(input.projectPath);
1523
+ // Normalized only for the readers that need it: every other use of
1524
+ // projectPath below reaches a helper that normalizes on its own, and doing it
1525
+ // here unconditionally would newly refuse a malformed path on calls that
1526
+ // never read one.
1527
+ const preferProjectVersionPath = input.preferProjectVersion
1528
+ ? normalizeOptionalProjectPath(input.projectPath)
1529
+ : undefined;
1530
+ if (preferProjectVersionPath && kind === "version") {
1531
+ const detected = await svc.workspaceMappingService.detectProjectMinecraftVersion(preferProjectVersionPath);
1365
1532
  if (detected && detected !== value) {
1366
1533
  warnings.push(`Overriding version "${value}" with project version "${detected}" from gradle.properties.`);
1367
1534
  }
@@ -1431,13 +1598,19 @@ export async function resolveArtifact(svc, input) {
1431
1598
  // below), so its mapping is never actually enforced either way.
1432
1599
  const versionNamesMinecraft = kind === "version" ||
1433
1600
  (kind === "coordinate" && !dependencyOrigin && coordinateIsMinecraftRuntime);
1434
- const minecraftVersion = versionNamesMinecraft ? resolvedVersion : undefined;
1435
- const runtimeNamesUnobfuscated = minecraftVersion !== undefined && isUnobfuscatedVersion(minecraftVersion);
1601
+ // A kind="jar" target names no version at all. It joins this gate only AFTER its
1602
+ // archive has been walked and proved to be a Minecraft 26.1+ runtime jar (see
1603
+ // `provenUnobfuscatedRuntimeJarVersion` below); until then it stays out.
1604
+ let minecraftVersion = versionNamesMinecraft ? resolvedVersion : undefined;
1605
+ let runtimeNamesUnobfuscated = minecraftVersion !== undefined && isUnobfuscatedVersion(minecraftVersion);
1436
1606
  let effectiveMapping = mapping;
1437
- if ((mapping === "intermediary" || mapping === "yarn") && runtimeNamesUnobfuscated) {
1438
- warnings.push(`Version ${minecraftVersion} is unobfuscated; ${mapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
1439
- effectiveMapping = "obfuscated";
1440
- }
1607
+ const dropInapplicableUnobfuscatedMapping = () => {
1608
+ if ((effectiveMapping === "intermediary" || effectiveMapping === "yarn") && runtimeNamesUnobfuscated) {
1609
+ warnings.push(`Version ${minecraftVersion} is unobfuscated; ${effectiveMapping} mappings are not applicable. Using the obfuscated namespace label for the deobfuscated runtime names.`);
1610
+ effectiveMapping = "obfuscated";
1611
+ }
1612
+ };
1613
+ dropInapplicableUnobfuscatedMapping();
1441
1614
  if (kind === "version" &&
1442
1615
  resolvedVersion &&
1443
1616
  effectiveMapping === "mojang" &&
@@ -1471,9 +1644,21 @@ export async function resolveArtifact(svc, input) {
1471
1644
  if (binaryRemapGate.warnings.length > 0) {
1472
1645
  warnings.push(...binaryRemapGate.warnings);
1473
1646
  }
1647
+ // Only a jar the caller named directly can be proved from its contents. A
1648
+ // dependency-origin jar is excluded for the reason given above, and a version
1649
+ // target is rewritten into a jar but already carries its version.
1650
+ const proveRuntimeJar = kind === "jar" && !dependencyOrigin;
1651
+ let subjectJarScan = undefined;
1474
1652
  const resolved = await resolveSourceTargetInternal(resolvedTarget, {
1475
1653
  allowDecompile: effectiveMapping === "mojang" ? true : input.allowDecompile ?? true,
1476
1654
  mappingVariant: binaryRemapGate.mappingVariant,
1655
+ ...(proveRuntimeJar
1656
+ ? {
1657
+ onSubjectJarScanned: (scan) => {
1658
+ subjectJarScan = scan;
1659
+ }
1660
+ }
1661
+ : {}),
1477
1662
  onRepoFailover: (event) => {
1478
1663
  svc.metrics.recordRepoFailover();
1479
1664
  log("warn", "repo.failover", {
@@ -1486,6 +1671,13 @@ export async function resolveArtifact(svc, input) {
1486
1671
  });
1487
1672
  }
1488
1673
  }, svc.config);
1674
+ const provenJarVersion = proveRuntimeJar ? provenUnobfuscatedRuntimeJarVersion(subjectJarScan) : undefined;
1675
+ if (provenJarVersion) {
1676
+ resolvedVersion = provenJarVersion;
1677
+ minecraftVersion = provenJarVersion;
1678
+ runtimeNamesUnobfuscated = true;
1679
+ dropInapplicableUnobfuscatedMapping();
1680
+ }
1489
1681
  resolved.version = resolvedVersion;
1490
1682
  let mappingDecision;
1491
1683
  try {
@@ -1547,9 +1739,21 @@ export async function resolveArtifact(svc, input) {
1547
1739
  mapping: effectiveMapping,
1548
1740
  target: { kind, value },
1549
1741
  nextAction: "Use target: { kind: \"version\", value } or a versioned Maven coordinate so mapping artifacts can be resolved.",
1742
+ // This branch is reached precisely because no version is known, and
1743
+ // `value` is whatever the caller's own target carried - a jar path
1744
+ // for the kind that gets here. Replaying it as a version target
1745
+ // produced a `suggestedCall` that could never succeed, so the
1746
+ // version goes out as a placeholder the caller must fill in;
1747
+ // buildSuggestedCall routes such a template to exampleCalls.
1550
1748
  ...buildSuggestedCall({
1551
1749
  tool: "resolve-artifact",
1552
- params: buildResolveArtifactParams({ kind: "version", value }, { ...(scope ? { scope } : {}) })
1750
+ params: undefined,
1751
+ examples: [
1752
+ {
1753
+ params: buildResolveArtifactParams({ kind: "version", value: "<your-mc-version>" }, { ...(scope ? { scope } : {}) }),
1754
+ reason: `Replace <your-mc-version> with the Minecraft version this artifact belongs to; ${effectiveMapping} mappings are resolved per version.`
1755
+ }
1756
+ ]
1553
1757
  })
1554
1758
  }
1555
1759
  });
@@ -1569,7 +1773,8 @@ export async function resolveArtifact(svc, input) {
1569
1773
  const provenance = buildProvenance({
1570
1774
  requestedTarget: { kind, value },
1571
1775
  resolved,
1572
- transformChain: [...mappingDecision.transformChain, ...additionalTransformChain]
1776
+ transformChain: [...mappingDecision.transformChain, ...additionalTransformChain],
1777
+ unobfuscatedRuntime: runtimeNamesUnobfuscated
1573
1778
  });
1574
1779
  if (workspaceProvenance) {
1575
1780
  provenance.workspaceResolution = workspaceProvenance;
@@ -17,6 +17,13 @@ export type RemapMembersInput = {
17
17
  signatureFields: SignatureMember[];
18
18
  signatureMethods: SignatureMember[];
19
19
  version: string | undefined;
20
+ /**
21
+ * `version` when it is a Minecraft version, else undefined: a dependency
22
+ * artifact's `version` is its own release number, which must not reach the
23
+ * 26.1+ obfuscated<->mojang identity shortcut, which may only be asked about a
24
+ * string proven to be a Minecraft version.
25
+ */
26
+ minecraftVersion: string | undefined;
20
27
  mappingApplied: SourceMapping;
21
28
  requestedMapping: SourceMapping;
22
29
  sourcePriority: MappingSourcePriority | undefined;
@@ -1,10 +1,13 @@
1
+ import { remapSignatureMembers } from "../lifecycle/mapping-helpers.js";
1
2
  import { matchesMemberPattern } from "../member-pattern.js";
2
3
  export async function remapAndCountMembers(svc, input) {
3
4
  const remap = async (members, kind) => {
4
5
  if (input.version == null) {
5
6
  return members;
6
7
  }
7
- const result = await svc.remapSignatureMembers(members, kind, input.version, input.mappingApplied, input.requestedMapping, input.sourcePriority, input.warnings, undefined, input.gradleUserHome);
8
+ // Called directly rather than through svc.remapSignatureMembers, whose fixed
9
+ // parameter list cannot carry minecraftVersion.
10
+ const result = await remapSignatureMembers(svc, members, kind, input.version, input.mappingApplied, input.requestedMapping, input.sourcePriority, input.warnings, undefined, input.gradleUserHome, { minecraftVersion: input.minecraftVersion });
8
11
  return result.members;
9
12
  };
10
13
  let constructors = await remap(input.signatureConstructors, "method");
@@ -21,7 +21,7 @@ import type { ArtifactProvenance, ArtifactScope, MappingSourcePriority, Resolved
21
21
  */
22
22
  export declare function reconcileUnobfuscatedNamespace(version: string | undefined, requestedMapping: SourceMapping, mappingApplied: SourceMapping): SourceMapping;
23
23
  export declare function resolveClassFilePath(svc: SourceService, artifactId: string, className: string): string | undefined;
24
- export declare function resolveClassNameForLookup(svc: SourceService, input: {
24
+ type ClassNameLookupInput = {
25
25
  className: string;
26
26
  version?: string;
27
27
  sourceMapping: SourceMapping;
@@ -30,7 +30,13 @@ export declare function resolveClassNameForLookup(svc: SourceService, input: {
30
30
  gradleUserHome?: string;
31
31
  warnings: string[];
32
32
  context: string;
33
- }): Promise<string>;
33
+ };
34
+ /**
35
+ * The class-name translation behind `svc.resolveClassNameForLookup`, whose callers
36
+ * (the lifecycle tools) pass a Minecraft version, so `version` also keys the 26.1+
37
+ * identity shortcut.
38
+ */
39
+ export declare function resolveClassNameForLookup(svc: SourceService, input: ClassNameLookupInput): Promise<string>;
34
40
  export declare function buildFallbackProvenance(svc: SourceService, input: {
35
41
  artifactId: string;
36
42
  origin: ResolvedSourceArtifact["origin"];
@@ -98,3 +104,4 @@ export declare function findClass(svc: SourceService, input: FindClassInput): Fi
98
104
  export declare function findClassIncludingNested(svc: SourceService, input: FindClassInput): Promise<FindClassOutput>;
99
105
  export declare function getClassSource(svc: SourceService, input: GetClassSourceInput): Promise<GetClassSourceOutput>;
100
106
  export declare function getClassMembers(svc: SourceService, input: GetClassMembersInput): Promise<GetClassMembersOutput>;
107
+ export {};