@adhisang/minecraft-modding-mcp 5.0.0 → 6.1.0

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 (83) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/dist/access-widener-parser.d.ts +1 -0
  3. package/dist/access-widener-parser.js +6 -3
  4. package/dist/cache-registry.js +35 -31
  5. package/dist/config.js +1 -1
  6. package/dist/decompiler/vineflower.js +8 -2
  7. package/dist/entry-tools/analyze-mod-service.js +2 -1
  8. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  9. package/dist/entry-tools/batch-class-members-service.d.ts +2 -0
  10. package/dist/entry-tools/batch-class-members-service.js +1 -0
  11. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  12. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  13. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  14. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  15. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  16. package/dist/entry-tools/response-contract.d.ts +2 -0
  17. package/dist/entry-tools/response-contract.js +2 -2
  18. package/dist/entry-tools/validate-project/cases/project-summary.js +30 -12
  19. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  20. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  21. package/dist/index.js +21 -16
  22. package/dist/java-process.js +1 -1
  23. package/dist/json-rpc-framing.d.ts +4 -0
  24. package/dist/json-rpc-framing.js +23 -1
  25. package/dist/mapping/internal-types.d.ts +17 -0
  26. package/dist/mapping/loaders/tiny-maven.js +46 -20
  27. package/dist/mapping/lookup.d.ts +12 -1
  28. package/dist/mapping/lookup.js +53 -1
  29. package/dist/mapping-service.js +37 -22
  30. package/dist/minecraft-explorer-service.js +2 -1
  31. package/dist/mixin/annotation-validators.js +21 -3
  32. package/dist/mixin/parsed-validator.js +1 -0
  33. package/dist/mixin-parser.js +71 -3
  34. package/dist/mod-decompile-service.d.ts +2 -0
  35. package/dist/mod-decompile-service.js +19 -2
  36. package/dist/mod-remap-service.js +6 -5
  37. package/dist/mojang-tiny-mapping-service.js +5 -2
  38. package/dist/nbt/java-nbt-codec.js +40 -16
  39. package/dist/nbt/json-patch.js +51 -12
  40. package/dist/nbt/typed-json.d.ts +1 -0
  41. package/dist/nbt/typed-json.js +12 -2
  42. package/dist/nbt/types.d.ts +2 -2
  43. package/dist/path-converter.js +10 -3
  44. package/dist/registry-service.d.ts +2 -0
  45. package/dist/registry-service.js +16 -1
  46. package/dist/repo-downloader.js +4 -3
  47. package/dist/source/artifact-resolver.js +1 -1
  48. package/dist/source/class-source/members-builder.d.ts +26 -0
  49. package/dist/source/class-source/members-builder.js +31 -4
  50. package/dist/source/class-source/snippet-builder.js +6 -0
  51. package/dist/source/class-source-helpers.d.ts +4 -4
  52. package/dist/source/class-source-helpers.js +12 -11
  53. package/dist/source/class-source.d.ts +19 -0
  54. package/dist/source/class-source.js +87 -26
  55. package/dist/source/file-access.js +3 -2
  56. package/dist/source/indexer.js +24 -7
  57. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  58. package/dist/source/lifecycle/runtime-check.js +20 -6
  59. package/dist/source/member-pattern.d.ts +16 -0
  60. package/dist/source/member-pattern.js +27 -0
  61. package/dist/source/search.js +23 -8
  62. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  63. package/dist/source/workspace-target.js +2 -1
  64. package/dist/source-resolver.js +2 -2
  65. package/dist/source-service.d.ts +8 -4
  66. package/dist/source-service.js +9 -1
  67. package/dist/stdio-supervisor.d.ts +2 -2
  68. package/dist/stdio-supervisor.js +29 -9
  69. package/dist/storage/db.js +24 -1
  70. package/dist/storage/files-repo.d.ts +3 -0
  71. package/dist/storage/files-repo.js +43 -37
  72. package/dist/storage/symbols-repo.d.ts +9 -0
  73. package/dist/storage/symbols-repo.js +47 -19
  74. package/dist/symbols/symbol-extractor.js +1 -1
  75. package/dist/tool-guidance.d.ts +2 -1
  76. package/dist/tool-guidance.js +27 -2
  77. package/dist/tool-schemas.d.ts +15 -4
  78. package/dist/tool-schemas.js +102 -97
  79. package/dist/warning-details.d.ts +12 -0
  80. package/dist/warning-details.js +17 -0
  81. package/dist/workspace-mapping-service.js +9 -0
  82. package/docs/tool-reference.md +23 -0
  83. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { mkdir, rename, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
+ import { randomBytes } from "node:crypto";
4
5
  import { buildSuggestedCall } from "./build-suggested-call.js";
5
6
  import { createError, ERROR_CODES } from "./errors.js";
6
7
  import { buildPageContextKey, encodeOffsetCursor, resolveCursorOffset } from "./page-cursor.js";
@@ -11,7 +12,7 @@ import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
11
12
  import { buildSymbolKey, createClassSymbolRecord, mergeDirectionIndexes, normalizeMappedSymbolOutput } from "./mapping/parsers/symbol-records.js";
12
13
  import { buildAdjacency, buildTargetRecordIndex, pairKey } from "./mapping/parsers/normalize.js";
13
14
  import { MATCH_RANK, MAX_CANDIDATES } from "./mapping/internal-types.js";
14
- import { applyDisambiguationHints, clampRowLimit, collectTargetRecords, effectiveLoomSearchProjectPath, inferAmbiguityReasons, invalidInputError, limitResolutionCandidates, lookupCandidates, mappingPriorityFromInput, mappingSourceOrder, namespacePath, normalizeIncludedKinds, normalizeQuerySymbol, pathToTransformChain, pathUsesSource, projectLookupCandidateDescriptor, requiresOnlyObfuscatedMojangGraph, toLookupCandidate, toResolutionCandidate } from "./mapping/lookup.js";
15
+ import { applyDisambiguationHints, clampRowLimit, collectExactRecordIndex, collectTargetRecords, effectiveLoomSearchProjectPath, inferAmbiguityReasons, invalidInputError, limitResolutionCandidates, lookupCandidates, lookupExactClassBySimpleName, lookupExactClassBySymbol, lookupExactMembers, lookupExactMembersByOwner, mappingPriorityFromInput, mappingSourceOrder, namespacePath, normalizeIncludedKinds, normalizeQuerySymbol, pathToTransformChain, pathUsesSource, projectLookupCandidateDescriptor, requiresOnlyObfuscatedMojangGraph, toLookupCandidate, toResolutionCandidate } from "./mapping/lookup.js";
15
16
  import { loadMojangPairs } from "./mapping/loaders/mojang.js";
16
17
  import { loadTinyPairsFromLoom } from "./mapping/loaders/tiny-loom.js";
17
18
  import { loadTinyPairsFromMaven } from "./mapping/loaders/tiny-maven.js";
@@ -375,7 +376,13 @@ export class MappingService {
375
376
  const candidates = rawCandidates.map(toResolutionCandidate);
376
377
  const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
377
378
  const strictDescriptor = projectedDescriptor ?? descriptor;
378
- const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor === strictDescriptor);
379
+ const acceptedDescriptors = new Set([descriptor, strictDescriptor]);
380
+ const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
381
+ if (toObfuscatedPath) {
382
+ const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, descriptor);
383
+ acceptedDescriptors.add(obfuscatedProjection.descriptor);
384
+ }
385
+ const strictCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
379
386
  if (strictCandidates.length === 1) {
380
387
  const resolved = toResolutionCandidate(strictCandidates[0]);
381
388
  return {
@@ -510,18 +517,18 @@ export class MappingService {
510
517
  warnings
511
518
  };
512
519
  }
513
- const baseRecords = collectTargetRecords(graph, baseMapping).filter((record) => {
514
- if (record.kind === "class") {
515
- return includeKinds.has("class") && record.symbol === baseClass.symbol;
516
- }
517
- if (record.owner !== baseClass.symbol) {
518
- return false;
520
+ const exactIndex = collectExactRecordIndex(graph, baseMapping);
521
+ const baseRecords = [];
522
+ if (includeKinds.has("class")) {
523
+ for (const record of lookupExactClassBySymbol(exactIndex, baseClass.symbol)) {
524
+ baseRecords.push(record);
519
525
  }
520
- if (record.kind === "field") {
521
- return includeKinds.has("field");
526
+ }
527
+ for (const record of lookupExactMembersByOwner(exactIndex, baseClass.symbol)) {
528
+ if (record.kind === "field" ? includeKinds.has("field") : includeKinds.has("method")) {
529
+ baseRecords.push(record);
522
530
  }
523
- return includeKinds.has("method");
524
- });
531
+ }
525
532
  const rowKindOrder = {
526
533
  class: 0,
527
534
  field: 1,
@@ -564,7 +571,8 @@ export class MappingService {
564
571
  className,
565
572
  classNameMapping,
566
573
  (input.includeKinds ?? []).join(","),
567
- input.sourcePriority
574
+ input.sourcePriority,
575
+ resolveGradleUserHomePath(input.gradleUserHome)
568
576
  ]);
569
577
  const { offset: rowOffset, cursorIgnored: rowCursorIgnored } = resolveCursorOffset(input.cursor, rowCursorContext);
570
578
  const windowBase = rowLimit != null ? dedupedBase.slice(rowOffset, rowOffset + rowLimit) : dedupedBase.slice(rowOffset);
@@ -573,7 +581,7 @@ export class MappingService {
573
581
  const nextCursor = rowsTruncated ? encodeOffsetCursor(consumedRows, rowCursorContext) : undefined;
574
582
  // Map ONLY the requested window. Per-row mapping cost is therefore O(window),
575
583
  // not O(rowCount). warnings/ambiguousRowCount are accordingly page-scoped on
576
- // paginated calls (caveat B1) and byte-identical to the old full-set values when
584
+ // paginated calls and byte-identical to the old full-set values when
577
585
  // no maxRows/cursor narrows the window.
578
586
  const rows = [];
579
587
  let ambiguousRowCount = 0;
@@ -709,7 +717,9 @@ export class MappingService {
709
717
  };
710
718
  })()
711
719
  : (() => {
712
- const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode);
720
+ const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
721
+ allowShortClassName: input.kind === "class" && sourceMapping === "obfuscated"
722
+ });
713
723
  return {
714
724
  mode: "strict",
715
725
  queryRecord,
@@ -730,6 +740,7 @@ export class MappingService {
730
740
  warnings
731
741
  };
732
742
  }
743
+ const exactIndex = collectExactRecordIndex(graph, sourceMapping);
733
744
  const buildOutput = (querySymbol, matched, status) => {
734
745
  const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
735
746
  const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
@@ -751,11 +762,11 @@ export class MappingService {
751
762
  if (normalizedQuery.mode === "auto-class") {
752
763
  const autoClassName = normalizedQuery.className;
753
764
  if (autoClassName.includes(".")) {
754
- const matched = records.filter((record) => record.kind === "class" && record.symbol === autoClassName);
765
+ const matched = lookupExactClassBySymbol(exactIndex, autoClassName);
755
766
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
756
767
  return buildOutput(normalizedQuery.querySymbol, matched, status);
757
768
  }
758
- const matched = records.filter((record) => record.kind === "class" && record.name === autoClassName);
769
+ const matched = lookupExactClassBySimpleName(exactIndex, autoClassName);
759
770
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
760
771
  if (status === "ambiguous") {
761
772
  warnings.push(`Multiple class symbols matched short name "${autoClassName}". Provide fully-qualified class name.`);
@@ -764,16 +775,16 @@ export class MappingService {
764
775
  }
765
776
  const { queryRecord, querySymbol } = normalizedQuery;
766
777
  if (queryRecord.kind === "class") {
767
- const matched = records.filter((record) => record.kind === "class" && record.symbol === queryRecord.symbol);
778
+ const matched = lookupExactClassBySymbol(exactIndex, queryRecord.symbol);
768
779
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
769
780
  return buildOutput(querySymbol, matched, status);
770
781
  }
771
782
  if (queryRecord.kind === "field") {
772
- const matched = records.filter((record) => record.kind === "field" && record.owner === queryRecord.owner && record.name === queryRecord.name);
783
+ const matched = lookupExactMembers(exactIndex, "field", queryRecord.owner ?? "", queryRecord.name);
773
784
  const status = matched.length === 1 ? "resolved" : matched.length > 1 ? "ambiguous" : "not_found";
774
785
  return buildOutput(querySymbol, matched, status);
775
786
  }
776
- const methodCandidates = records.filter((record) => record.kind === "method" && record.owner === queryRecord.owner && record.name === queryRecord.name);
787
+ const methodCandidates = lookupExactMembers(exactIndex, "method", queryRecord.owner ?? "", queryRecord.name);
777
788
  // name-only mode: skip descriptor matching, resolve by owner+name
778
789
  if (effectiveSignatureMode === "name-only") {
779
790
  const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
@@ -1102,6 +1113,7 @@ export class MappingService {
1102
1113
  adjacency: new Map(),
1103
1114
  pathCache: new Map(),
1104
1115
  recordsByTarget: new Map(),
1116
+ exactRecordIndex: new Map(),
1105
1117
  classProjectionCache: new Map(),
1106
1118
  warnings: [
1107
1119
  `Version ${version} is unobfuscated; mapping graph is empty because the runtime already uses deobfuscated names.`
@@ -1116,6 +1128,7 @@ export class MappingService {
1116
1128
  adjacency: new Map(),
1117
1129
  pathCache: new Map(),
1118
1130
  recordsByTarget: new Map(),
1131
+ exactRecordIndex: new Map(),
1119
1132
  classProjectionCache: new Map(),
1120
1133
  warnings: []
1121
1134
  };
@@ -1282,7 +1295,9 @@ async function extractTinyFromJar(jarPath, outputPath) {
1282
1295
  return false;
1283
1296
  }
1284
1297
  await mkdir(dirname(outputPath), { recursive: true });
1285
- await writeFile(outputPath, tinyEntry.content, "utf8");
1298
+ const tempPath = `${outputPath}.${randomBytes(4).toString("hex")}.tmp`;
1299
+ await writeFile(tempPath, tinyEntry.content, "utf8");
1300
+ await rename(tempPath, outputPath);
1286
1301
  return true;
1287
1302
  }
1288
1303
  /**
@@ -2,6 +2,7 @@ import { createError, ERROR_CODES } from "./errors.js";
2
2
  import { loadConfig } from "./config.js";
3
3
  import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
4
4
  import { createJarEntryReader } from "./source-jar-reader.js";
5
+ import { matchesMemberPattern } from "./source/member-pattern.js";
5
6
  const CLASSFILE_MAGIC = 0xcafebabe;
6
7
  const MAX_INHERITANCE_DEPTH = 64;
7
8
  const ACC_PUBLIC = 0x0001;
@@ -603,7 +604,7 @@ export class MinecraftExplorerService {
603
604
  if (access === "public" && !hasPublicVisibility(member.accessFlags)) {
604
605
  return false;
605
606
  }
606
- if (memberPattern && !lower(member.name).includes(memberPattern)) {
607
+ if (memberPattern && !matchesMemberPattern(member.name, memberPattern)) {
607
608
  return false;
608
609
  }
609
610
  return true;
@@ -6,8 +6,26 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
6
6
  continue;
7
7
  const methodNames = allMethodNames(members);
8
8
  // Strip owner prefix and JVM descriptor from the method reference
9
- const methodName = extractMethodName(inj.method);
10
- if (!methodNames.includes(methodName)) {
9
+ const rawMethodName = extractMethodName(inj.method);
10
+ // Strip a trailing target-selector quantifier (e.g. "tick*", "tick+", "tick{2,3}")
11
+ const quantifier = /(?:\*|\+|\{\d+(?:,\d+)?\})$/.exec(rawMethodName)?.[0];
12
+ const methodName = quantifier
13
+ ? rawMethodName.slice(0, rawMethodName.length - quantifier.length)
14
+ : rawMethodName;
15
+ if (quantifier && methodName === "") {
16
+ // Bare wildcard selector (e.g. method = "*") matches everything; unverifiable.
17
+ resolvedMembers.push({
18
+ annotation: `@${inj.annotation}`,
19
+ name: rawMethodName,
20
+ line: inj.line,
21
+ status: "skipped"
22
+ });
23
+ continue;
24
+ }
25
+ const matched = (quantifier === "*" || quantifier === "+")
26
+ ? methodNames.some((n) => n.startsWith(methodName))
27
+ : methodNames.includes(methodName);
28
+ if (!matched) {
11
29
  const suggestions = suggestSimilar(methodName, methodNames);
12
30
  const descriptor = extractMethodDescriptor(inj.method);
13
31
  const descriptorHint = descriptor ? ` (descriptor: ${descriptor})` : "";
@@ -46,7 +64,7 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
46
64
  // Name matched. If the reference carries a JVM descriptor, require an
47
65
  // overload to match it; otherwise a wrong-signature target (e.g. tick(D)V
48
66
  // when only tick(I)V exists) is silently reported as resolved.
49
- const refDescriptor = extractMethodDescriptor(inj.method);
67
+ const refDescriptor = quantifier ? undefined : extractMethodDescriptor(inj.method);
50
68
  const sameNameMembers = [...members.constructors, ...members.methods].filter((member) => member.name === methodName);
51
69
  if (refDescriptor &&
52
70
  sameNameMembers.length > 0 &&
@@ -27,6 +27,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
27
27
  resolutionPath: "target-mapping-failed",
28
28
  falsePositiveRisk: healthReport?.overallHealthy === false ? "high" : "medium"
29
29
  });
30
+ addSkippedMembers(parsed, resolvedMembers);
30
31
  }
31
32
  else if (symbolExistsButSignatureFailed?.has(target.className)) {
32
33
  // Symbol exists in mapping graph but getSignature failed — tool limitation, not code issue
@@ -5,13 +5,15 @@
5
5
  /* ------------------------------------------------------------------ */
6
6
  /* Regex patterns */
7
7
  /* ------------------------------------------------------------------ */
8
- const CLASS_DECL_RE = /(?:public\s+)?(?:abstract\s+)?(?:class|interface)\s+(\w+)/;
8
+ const CLASS_DECL_RE = /^\s*(?:@[\w$.]+(?:\([^)]*\))?\s+)*(?:public\s+|final\s+|abstract\s+|sealed\s+)*\b(?:class|interface)\s+(\w+)/;
9
9
  // import statements for FQCN resolution
10
10
  const IMPORT_RE = /^\s*import\s+([\w.]+)\s*;/;
11
11
  // @Mixin(Foo.class) or @Mixin({Foo.class, Bar.class}) or @Mixin(value = Foo.class)
12
12
  // Also handles @Mixin(value = {Foo.class, Bar.class}, priority = 900)
13
13
  const MIXIN_ANNOTATION_START_RE = /^\s*@Mixin\s*\(/;
14
- const MIXIN_TARGET_RE = /(\w[\w.]*?)\.class/g;
14
+ // `$` is a legal Java identifier character (e.g. generated classes like My$Class);
15
+ // without it the capture would start mid-name and yield a wrong target.
16
+ const MIXIN_TARGET_RE = /([\w$][\w.$]*?)\.class/g;
15
17
  const MIXIN_PRIORITY_RE = /priority\s*=\s*(\d+)/;
16
18
  // String-form targets: @Mixin(targets = "pkg.Class") or @Mixin(targets = {"pkg.A", "pkg.B"})
17
19
  const MIXIN_TARGETS_STRING_RE = /targets\s*=\s*(?:\{([^}]+)\}|"([^"]+)")/;
@@ -55,6 +57,62 @@ function collectMultilineAnnotation(lines, startIndex) {
55
57
  }
56
58
  return { text, endIndex: lines.length - 1 };
57
59
  }
60
+ /**
61
+ * Blank out `/* *\/` block comments and `//` line comments (outside string literals)
62
+ * while preserving line count and line numbers, so the annotation scans never match
63
+ * inside commented-out code.
64
+ */
65
+ function stripComments(source) {
66
+ let out = "";
67
+ let inBlock = false;
68
+ let inString = false;
69
+ for (let i = 0; i < source.length; i++) {
70
+ const ch = source[i];
71
+ const next = source[i + 1];
72
+ if (inBlock) {
73
+ if (ch === "*" && next === "/") {
74
+ out += " ";
75
+ i++;
76
+ inBlock = false;
77
+ }
78
+ else {
79
+ out += ch === "\n" ? "\n" : " ";
80
+ }
81
+ continue;
82
+ }
83
+ if (inString) {
84
+ out += ch;
85
+ if (ch === "\\" && next !== undefined) {
86
+ out += next;
87
+ i++;
88
+ }
89
+ else if (ch === '"') {
90
+ inString = false;
91
+ }
92
+ continue;
93
+ }
94
+ if (ch === '"') {
95
+ inString = true;
96
+ out += ch;
97
+ continue;
98
+ }
99
+ if (ch === "/" && next === "*") {
100
+ out += " ";
101
+ i++;
102
+ inBlock = true;
103
+ continue;
104
+ }
105
+ if (ch === "/" && next === "/") {
106
+ while (i < source.length && source[i] !== "\n") {
107
+ i++;
108
+ }
109
+ i--;
110
+ continue;
111
+ }
112
+ out += ch;
113
+ }
114
+ return out;
115
+ }
58
116
  /**
59
117
  * Skip past annotations (including multi-line ones) starting at `startIndex`.
60
118
  * Returns the index of the first non-annotation line.
@@ -169,7 +227,7 @@ function inferAccessorTarget(methodName) {
169
227
  /* Main parser */
170
228
  /* ------------------------------------------------------------------ */
171
229
  export function parseMixinSource(source) {
172
- const lines = source.split(/\r?\n/);
230
+ const lines = stripComments(source).split(/\r?\n/);
173
231
  const parseWarnings = [];
174
232
  const targets = [];
175
233
  const injections = [];
@@ -323,6 +381,16 @@ export function parseMixinSource(source) {
323
381
  }
324
382
  i = endIndex;
325
383
  }
384
+ // Try same-line declaration first (e.g. `@Accessor("x") int getX();`)
385
+ const inlineDecl = stripInlineAnnotations(line);
386
+ const inlineMethodMatch = inlineDecl.includes("(") ? METHOD_DECL_RE.exec(inlineDecl) : null;
387
+ if (inlineMethodMatch) {
388
+ const methodName = inlineMethodMatch[2];
389
+ const targetName = explicitTarget ?? inferAccessorTarget(methodName);
390
+ accessors.push({ annotation, name: methodName, targetName, line: lineNum });
391
+ i++;
392
+ continue;
393
+ }
326
394
  // Find the method declaration following the annotation (skip multi-line annotations)
327
395
  let methodLine = i + 1;
328
396
  methodLine = skipAnnotations(lines, methodLine);
@@ -43,8 +43,10 @@ export type GetModClassSourceOutput = {
43
43
  export declare class ModDecompileService {
44
44
  private readonly config;
45
45
  private readonly decompileCache;
46
+ private readonly inflightDecompiles;
46
47
  constructor(config: Config);
47
48
  decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
48
49
  getModClassSource(input: GetModClassSourceInput): Promise<GetModClassSourceOutput>;
49
50
  private ensureDecompiled;
51
+ private decompileAndCache;
50
52
  }
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { statSync } from "node:fs";
2
3
  import { readFile, writeFile } from "node:fs/promises";
3
4
  import { isAbsolute, join, resolve as resolvePath } from "node:path";
4
5
  import { createError, ERROR_CODES } from "./errors.js";
@@ -6,11 +7,12 @@ import { log } from "./logger.js";
6
7
  import { decompileBinaryJar } from "./decompiler/vineflower.js";
7
8
  import { resolveVineflowerJar } from "./vineflower-resolver.js";
8
9
  import { analyzeModJar } from "./mod-analyzer.js";
9
- import { validateAndNormalizeJarPath } from "./path-resolver.js";
10
+ import { buildJarSignature, validateAndNormalizeJarPath } from "./path-resolver.js";
10
11
  import { sliceToMaxCharsSafe } from "./text-truncate.js";
11
12
  const DECOMPILE_TIMEOUT_MS = 300_000;
12
13
  function modDecompileCacheKey(jarPath) {
13
- return createHash("sha256").update(jarPath).digest("hex");
14
+ const stats = statSync(jarPath);
15
+ return createHash("sha256").update(`${jarPath}|${buildJarSignature(stats)}`).digest("hex");
14
16
  }
15
17
  function classNameToFilePath(className) {
16
18
  return className.replaceAll(".", "/") + ".java";
@@ -28,6 +30,7 @@ export class ModDecompileService {
28
30
  config;
29
31
  // Cache: jarPath hash → decompiled output dir + file list
30
32
  decompileCache = new Map();
33
+ inflightDecompiles = new Map();
31
34
  constructor(config) {
32
35
  this.config = config;
33
36
  }
@@ -155,6 +158,20 @@ export class ModDecompileService {
155
158
  this.decompileCache.set(cacheKey, cached);
156
159
  return cached;
157
160
  }
161
+ const existingDecompile = this.inflightDecompiles.get(cacheKey);
162
+ if (existingDecompile) {
163
+ return existingDecompile;
164
+ }
165
+ const decompilePromise = this.decompileAndCache(jarPath, cacheKey, warnings);
166
+ this.inflightDecompiles.set(cacheKey, decompilePromise);
167
+ try {
168
+ return await decompilePromise;
169
+ }
170
+ finally {
171
+ this.inflightDecompiles.delete(cacheKey);
172
+ }
173
+ }
174
+ async decompileAndCache(jarPath, cacheKey, warnings) {
158
175
  log("info", "mod-decompile.start", { jarPath });
159
176
  const startedAt = Date.now();
160
177
  // Analyze mod metadata
@@ -1,5 +1,5 @@
1
- import { createHash } from "node:crypto";
2
- import { copyFileSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, statSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { createError, ERROR_CODES, isAppError } from "./errors.js";
@@ -225,8 +225,7 @@ export async function remapModJar(input, config, deps = {}) {
225
225
  }
226
226
  mkdirSync(dirname(outputJar), { recursive: true });
227
227
  // 7. Use a temporary directory for intermediate work
228
- const tempDir = join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}`);
229
- mkdirSync(tempDir, { recursive: true });
228
+ const tempDir = mkdtempSync(join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}-`));
230
229
  try {
231
230
  let passInput = normalizedInput;
232
231
  let tempOutput = "";
@@ -266,7 +265,9 @@ export async function remapModJar(input, config, deps = {}) {
266
265
  copyFileSync(tempOutput, outputJar);
267
266
  if (outputJar !== cachedOutput) {
268
267
  mkdirSync(dirname(cachedOutput), { recursive: true });
269
- copyFileSync(tempOutput, cachedOutput);
268
+ const tempCachedOutput = `${cachedOutput}.${randomBytes(4).toString("hex")}.tmp`;
269
+ copyFileSync(tempOutput, tempCachedOutput);
270
+ renameSync(tempCachedOutput, cachedOutput);
270
271
  }
271
272
  const durationMs = Date.now() - startedAt;
272
273
  log("info", "remap.pipeline.done", {
@@ -1,5 +1,6 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { existsSync } from "node:fs";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
4
  import { dirname, join } from "node:path";
4
5
  import { createError, ERROR_CODES } from "./errors.js";
5
6
  import { log } from "./logger.js";
@@ -339,7 +340,9 @@ export async function resolveMojangTinyFile(version, config, deps = {}) {
339
340
  const membersByOwner = normalizeMemberMappings(parsedMappings, classMap, warnings);
340
341
  const tinyContent = renderTinyV2(classMap, membersByOwner);
341
342
  await mkdir(dirname(cachedTiny), { recursive: true });
342
- await writeFile(cachedTiny, tinyContent, "utf8");
343
+ const tempTiny = `${cachedTiny}.${randomBytes(4).toString("hex")}.tmp`;
344
+ await writeFile(tempTiny, tinyContent, "utf8");
345
+ await rename(tempTiny, cachedTiny);
343
346
  log("info", "mojang-tiny.generated", {
344
347
  version,
345
348
  path: cachedTiny,
@@ -225,12 +225,12 @@ class NbtWriter {
225
225
  }
226
226
  writeFloat32(value) {
227
227
  const chunk = Buffer.allocUnsafe(4);
228
- chunk.writeFloatBE(value, 0);
228
+ chunk.writeFloatBE(Number(value), 0);
229
229
  this.chunks.push(chunk);
230
230
  }
231
231
  writeFloat64(value) {
232
232
  const chunk = Buffer.allocUnsafe(8);
233
- chunk.writeDoubleBE(value, 0);
233
+ chunk.writeDoubleBE(Number(value), 0);
234
234
  this.chunks.push(chunk);
235
235
  }
236
236
  writeInt64(value) {
@@ -238,6 +238,27 @@ class NbtWriter {
238
238
  chunk.writeBigInt64BE(value, 0);
239
239
  this.chunks.push(chunk);
240
240
  }
241
+ writeInt8Array(values) {
242
+ const chunk = Buffer.allocUnsafe(values.length);
243
+ for (let i = 0; i < values.length; i += 1) {
244
+ chunk.writeInt8(values[i], i);
245
+ }
246
+ this.chunks.push(chunk);
247
+ }
248
+ writeInt32Array(values) {
249
+ const chunk = Buffer.allocUnsafe(values.length * 4);
250
+ for (let i = 0; i < values.length; i += 1) {
251
+ chunk.writeInt32BE(values[i], i * 4);
252
+ }
253
+ this.chunks.push(chunk);
254
+ }
255
+ writeInt64Array(values) {
256
+ const chunk = Buffer.allocUnsafe(values.length * 8);
257
+ for (let i = 0; i < values.length; i += 1) {
258
+ chunk.writeBigInt64BE(values[i], i * 8);
259
+ }
260
+ this.chunks.push(chunk);
261
+ }
241
262
  writeString(value) {
242
263
  const encoded = encodeMutf8(value);
243
264
  if (encoded.length > 0xffff) {
@@ -261,10 +282,14 @@ function readPayload(reader, tagId, pointer) {
261
282
  return { type: "int", value: reader.readInt32() };
262
283
  case "long":
263
284
  return { type: "long", value: reader.readInt64().toString() };
264
- case "float":
265
- return { type: "float", value: reader.readFloat32() };
266
- case "double":
267
- return { type: "double", value: reader.readFloat64() };
285
+ case "float": {
286
+ const value = reader.readFloat32();
287
+ return { type: "float", value: Number.isFinite(value) ? value : Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity" };
288
+ }
289
+ case "double": {
290
+ const value = reader.readFloat64();
291
+ return { type: "double", value: Number.isFinite(value) ? value : Number.isNaN(value) ? "NaN" : value > 0 ? "Infinity" : "-Infinity" };
292
+ }
268
293
  case "byteArray": {
269
294
  const length = reader.readInt32();
270
295
  if (length < 0) {
@@ -315,7 +340,12 @@ function readPayload(reader, tagId, pointer) {
315
340
  break;
316
341
  }
317
342
  const childName = reader.readString();
318
- value[childName] = readPayload(reader, childType, `${pointer}/value/${childName}`);
343
+ Object.defineProperty(value, childName, {
344
+ value: readPayload(reader, childType, `${pointer}/value/${childName}`),
345
+ enumerable: true,
346
+ writable: true,
347
+ configurable: true
348
+ });
319
349
  }
320
350
  return { type: "compound", value };
321
351
  }
@@ -380,9 +410,7 @@ function writePayload(writer, node, pointer) {
380
410
  return;
381
411
  case "byteArray":
382
412
  writer.writeInt32(node.value.length);
383
- for (const value of node.value) {
384
- writer.writeInt8(value);
385
- }
413
+ writer.writeInt8Array(node.value);
386
414
  return;
387
415
  case "string":
388
416
  writer.writeString(node.value);
@@ -404,15 +432,11 @@ function writePayload(writer, node, pointer) {
404
432
  return;
405
433
  case "intArray":
406
434
  writer.writeInt32(node.value.length);
407
- for (const value of node.value) {
408
- writer.writeInt32(value);
409
- }
435
+ writer.writeInt32Array(node.value);
410
436
  return;
411
437
  case "longArray":
412
438
  writer.writeInt32(node.value.length);
413
- for (const value of node.value) {
414
- writer.writeInt64(BigInt(value));
415
- }
439
+ writer.writeInt64Array(node.value.map((value) => BigInt(value)));
416
440
  return;
417
441
  default:
418
442
  throw encodeError("Unsupported typed NBT node for encoding.", {