@adhisang/minecraft-modding-mcp 5.0.0 → 6.0.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 (72) hide show
  1. package/CHANGELOG.md +36 -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/decompiler/vineflower.js +8 -2
  6. package/dist/entry-tools/analyze-mod-service.js +2 -1
  7. package/dist/entry-tools/analyze-symbol-service.js +18 -9
  8. package/dist/entry-tools/compare-minecraft-service.js +1 -1
  9. package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -1
  10. package/dist/entry-tools/inspect-minecraft/handlers/file.js +26 -1
  11. package/dist/entry-tools/inspect-minecraft/handlers/versions.js +2 -1
  12. package/dist/entry-tools/inspect-minecraft/internal.js +2 -0
  13. package/dist/entry-tools/response-contract.d.ts +2 -0
  14. package/dist/entry-tools/response-contract.js +2 -2
  15. package/dist/entry-tools/validate-project-service.d.ts +4 -4
  16. package/dist/entry-tools/verify-mixin-target-service.js +7 -7
  17. package/dist/index.js +20 -16
  18. package/dist/java-process.js +1 -1
  19. package/dist/json-rpc-framing.d.ts +4 -0
  20. package/dist/json-rpc-framing.js +23 -1
  21. package/dist/mapping/internal-types.d.ts +17 -0
  22. package/dist/mapping/loaders/tiny-maven.js +46 -20
  23. package/dist/mapping/lookup.d.ts +12 -1
  24. package/dist/mapping/lookup.js +53 -1
  25. package/dist/mapping-service.js +36 -21
  26. package/dist/mixin/annotation-validators.js +21 -3
  27. package/dist/mixin/parsed-validator.js +1 -0
  28. package/dist/mixin-parser.js +71 -3
  29. package/dist/mod-decompile-service.d.ts +2 -0
  30. package/dist/mod-decompile-service.js +19 -2
  31. package/dist/mod-remap-service.js +6 -5
  32. package/dist/mojang-tiny-mapping-service.js +5 -2
  33. package/dist/nbt/java-nbt-codec.js +40 -16
  34. package/dist/nbt/json-patch.js +51 -12
  35. package/dist/nbt/typed-json.d.ts +1 -0
  36. package/dist/nbt/typed-json.js +12 -2
  37. package/dist/nbt/types.d.ts +2 -2
  38. package/dist/path-converter.js +10 -3
  39. package/dist/registry-service.d.ts +2 -0
  40. package/dist/registry-service.js +16 -1
  41. package/dist/repo-downloader.js +4 -3
  42. package/dist/source/artifact-resolver.js +1 -1
  43. package/dist/source/class-source/snippet-builder.js +6 -0
  44. package/dist/source/class-source-helpers.d.ts +4 -4
  45. package/dist/source/class-source-helpers.js +12 -11
  46. package/dist/source/class-source.d.ts +19 -0
  47. package/dist/source/class-source.js +48 -21
  48. package/dist/source/file-access.js +3 -2
  49. package/dist/source/indexer.js +24 -7
  50. package/dist/source/lifecycle/mapping-helpers.js +28 -3
  51. package/dist/source/lifecycle/runtime-check.js +20 -6
  52. package/dist/source/search.js +23 -8
  53. package/dist/source/validate-mixin/pipeline/resolve.js +23 -3
  54. package/dist/source/workspace-target.js +2 -1
  55. package/dist/source-resolver.js +2 -2
  56. package/dist/source-service.js +9 -1
  57. package/dist/stdio-supervisor.d.ts +2 -2
  58. package/dist/stdio-supervisor.js +29 -9
  59. package/dist/storage/db.js +24 -1
  60. package/dist/storage/files-repo.d.ts +3 -0
  61. package/dist/storage/files-repo.js +43 -37
  62. package/dist/storage/symbols-repo.d.ts +9 -0
  63. package/dist/storage/symbols-repo.js +47 -19
  64. package/dist/symbols/symbol-extractor.js +1 -1
  65. package/dist/tool-guidance.d.ts +2 -1
  66. package/dist/tool-guidance.js +26 -1
  67. package/dist/tool-schemas.d.ts +4 -4
  68. package/dist/tool-schemas.js +91 -91
  69. package/dist/warning-details.d.ts +12 -0
  70. package/dist/warning-details.js +17 -0
  71. package/dist/workspace-mapping-service.js +9 -0
  72. package/package.json +1 -1
@@ -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.", {
@@ -1,6 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { createError, ERROR_CODES } from "../errors.js";
3
- import { assertValidTypedNbtDocument, validateTypedNbtDocument } from "./typed-json.js";
3
+ import { assertValidTypedNbtDocument, validateTypedNbtDocument, validateTypedNbtNode } from "./typed-json.js";
4
4
  function isRecord(value) {
5
5
  return (typeof value === "object" &&
6
6
  value !== null &&
@@ -136,6 +136,9 @@ function parseArrayIndex(token, length, options) {
136
136
  }
137
137
  return index;
138
138
  }
139
+ function isTypedNbtNode(value) {
140
+ return isRecord(value) && typeof value.type === "string";
141
+ }
139
142
  function resolveParent(root, tokens, opIndex, path) {
140
143
  if (tokens.length === 0) {
141
144
  patchConflict("Operation path does not reference a child location.", {
@@ -144,6 +147,8 @@ function resolveParent(root, tokens, opIndex, path) {
144
147
  });
145
148
  }
146
149
  let cursor = root;
150
+ let enclosingNode;
151
+ let enclosingPointer = "";
147
152
  for (let i = 0; i < tokens.length - 1; i += 1) {
148
153
  const token = tokens[i];
149
154
  const pointer = `/${tokens.slice(0, i + 1).join("/")}`;
@@ -154,6 +159,10 @@ function resolveParent(root, tokens, opIndex, path) {
154
159
  path: pointer
155
160
  });
156
161
  cursor = cursor[index];
162
+ if (isTypedNbtNode(cursor)) {
163
+ enclosingNode = cursor;
164
+ enclosingPointer = pointer;
165
+ }
157
166
  continue;
158
167
  }
159
168
  if (!isRecord(cursor)) {
@@ -169,8 +178,12 @@ function resolveParent(root, tokens, opIndex, path) {
169
178
  });
170
179
  }
171
180
  cursor = cursor[token];
181
+ if (isTypedNbtNode(cursor)) {
182
+ enclosingNode = cursor;
183
+ enclosingPointer = pointer;
184
+ }
172
185
  }
173
- return { parent: cursor, key: tokens[tokens.length - 1] };
186
+ return { parent: cursor, key: tokens[tokens.length - 1], enclosingNode, enclosingPointer };
174
187
  }
175
188
  function readValueAtPath(root, tokens, opIndex, path) {
176
189
  if (tokens.length === 0) {
@@ -205,8 +218,10 @@ function readValueAtPath(root, tokens, opIndex, path) {
205
218
  }
206
219
  return cursor;
207
220
  }
208
- function assertTypedNbtInvariant(root, opIndex, path) {
209
- const validation = validateTypedNbtDocument(root);
221
+ function assertTypedNbtInvariant(subtree, opIndex, path) {
222
+ const validation = "root" in subtree
223
+ ? validateTypedNbtDocument(subtree.root)
224
+ : validateTypedNbtNode(subtree.node, subtree.pointer);
210
225
  if (!validation.ok) {
211
226
  patchConflict("JSON Patch operation produced invalid typed NBT JSON.", {
212
227
  opIndex,
@@ -241,11 +256,13 @@ export function applyJsonPatch(document, patch) {
241
256
  }
242
257
  if (operation.op === "add") {
243
258
  const nextValue = structuredClone(operation.value);
259
+ let subtree = { root: working };
244
260
  if (tokens.length === 0) {
245
261
  working = nextValue;
262
+ subtree = { root: working };
246
263
  }
247
264
  else {
248
- const { parent, key } = resolveParent(working, tokens, i, operation.path);
265
+ const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
249
266
  if (Array.isArray(parent)) {
250
267
  const index = parseArrayIndex(key, parent.length, {
251
268
  allowAppend: true,
@@ -255,7 +272,12 @@ export function applyJsonPatch(document, patch) {
255
272
  parent.splice(index, 0, nextValue);
256
273
  }
257
274
  else if (isRecord(parent)) {
258
- parent[key] = nextValue;
275
+ Object.defineProperty(parent, key, {
276
+ value: nextValue,
277
+ enumerable: true,
278
+ writable: true,
279
+ configurable: true
280
+ });
259
281
  }
260
282
  else {
261
283
  patchConflict("Add target parent is not a container.", {
@@ -263,9 +285,13 @@ export function applyJsonPatch(document, patch) {
263
285
  jsonPointer: operation.path
264
286
  });
265
287
  }
288
+ subtree =
289
+ enclosingNode === undefined
290
+ ? { root: working }
291
+ : { node: enclosingNode, pointer: enclosingPointer };
266
292
  }
267
293
  changed = true;
268
- assertTypedNbtInvariant(working, i, operation.path);
294
+ assertTypedNbtInvariant(subtree, i, operation.path);
269
295
  continue;
270
296
  }
271
297
  if (operation.op === "remove") {
@@ -275,7 +301,7 @@ export function applyJsonPatch(document, patch) {
275
301
  jsonPointer: operation.path
276
302
  });
277
303
  }
278
- const { parent, key } = resolveParent(working, tokens, i, operation.path);
304
+ const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
279
305
  if (Array.isArray(parent)) {
280
306
  const index = parseArrayIndex(key, parent.length, {
281
307
  allowAppend: false,
@@ -300,16 +326,20 @@ export function applyJsonPatch(document, patch) {
300
326
  });
301
327
  }
302
328
  changed = true;
303
- assertTypedNbtInvariant(working, i, operation.path);
329
+ assertTypedNbtInvariant(enclosingNode === undefined
330
+ ? { root: working }
331
+ : { node: enclosingNode, pointer: enclosingPointer }, i, operation.path);
304
332
  continue;
305
333
  }
306
334
  if (operation.op === "replace") {
307
335
  const nextValue = structuredClone(operation.value);
336
+ let subtree = { root: working };
308
337
  if (tokens.length === 0) {
309
338
  working = nextValue;
339
+ subtree = { root: working };
310
340
  }
311
341
  else {
312
- const { parent, key } = resolveParent(working, tokens, i, operation.path);
342
+ const { parent, key, enclosingNode, enclosingPointer } = resolveParent(working, tokens, i, operation.path);
313
343
  if (Array.isArray(parent)) {
314
344
  const index = parseArrayIndex(key, parent.length, {
315
345
  allowAppend: false,
@@ -325,7 +355,12 @@ export function applyJsonPatch(document, patch) {
325
355
  jsonPointer: operation.path
326
356
  });
327
357
  }
328
- parent[key] = nextValue;
358
+ Object.defineProperty(parent, key, {
359
+ value: nextValue,
360
+ enumerable: true,
361
+ writable: true,
362
+ configurable: true
363
+ });
329
364
  }
330
365
  else {
331
366
  patchConflict("Replace target parent is not a container.", {
@@ -333,9 +368,13 @@ export function applyJsonPatch(document, patch) {
333
368
  jsonPointer: operation.path
334
369
  });
335
370
  }
371
+ subtree =
372
+ enclosingNode === undefined
373
+ ? { root: working }
374
+ : { node: enclosingNode, pointer: enclosingPointer };
336
375
  }
337
376
  changed = true;
338
- assertTypedNbtInvariant(working, i, operation.path);
377
+ assertTypedNbtInvariant(subtree, i, operation.path);
339
378
  continue;
340
379
  }
341
380
  }
@@ -5,6 +5,7 @@ type ValidationResult = {
5
5
  ok: false;
6
6
  issue: TypedNbtValidationIssue;
7
7
  };
8
+ export declare function validateTypedNbtNode(value: unknown, pointer: string): ValidationResult;
8
9
  export declare function validateTypedNbtDocument(value: unknown): ValidationResult;
9
10
  export declare function assertValidTypedNbtDocument(value: unknown): asserts value is TypedNbtDocument;
10
11
  export type { TypedNbtDocument, TypedNbtValidationIssue } from "./types.js";
@@ -16,6 +16,7 @@ const NODE_TYPES = [
16
16
  const LIST_ELEMENT_TYPES = ["end", ...NODE_TYPES];
17
17
  const NODE_TYPE_SET = new Set(NODE_TYPES);
18
18
  const LIST_ELEMENT_TYPE_SET = new Set(LIST_ELEMENT_TYPES);
19
+ const NON_FINITE_FLOAT_SENTINELS = new Set(["NaN", "Infinity", "-Infinity"]);
19
20
  const INT64_MIN = -9223372036854775808n;
20
21
  const INT64_MAX = 9223372036854775807n;
21
22
  function isRecord(value) {
@@ -119,8 +120,14 @@ function validateNode(value, pointer) {
119
120
  return validateLongString(node.value, `${pointer}/value`);
120
121
  case "float":
121
122
  case "double":
122
- if (typeof node.value !== "number" || !Number.isFinite(node.value)) {
123
- return fail(`${pointer}/value`, "finite-number", node.value);
123
+ if (typeof node.value === "string") {
124
+ if (!NON_FINITE_FLOAT_SENTINELS.has(node.value)) {
125
+ return fail(`${pointer}/value`, "number-or-non-finite-sentinel", node.value);
126
+ }
127
+ return { ok: true };
128
+ }
129
+ if (typeof node.value !== "number") {
130
+ return fail(`${pointer}/value`, "number-or-non-finite-sentinel", node.value);
124
131
  }
125
132
  return { ok: true };
126
133
  case "string":
@@ -180,6 +187,9 @@ function validateNode(value, pointer) {
180
187
  return fail(`${pointer}/type`, "nbt-node-type", nodeType);
181
188
  }
182
189
  }
190
+ export function validateTypedNbtNode(value, pointer) {
191
+ return validateNode(value, pointer);
192
+ }
183
193
  export function validateTypedNbtDocument(value) {
184
194
  if (!isRecord(value)) {
185
195
  return fail("", "typed-nbt-document", value);
@@ -15,10 +15,10 @@ export type NbtNode = {
15
15
  value: string;
16
16
  } | {
17
17
  type: "float";
18
- value: number;
18
+ value: number | "NaN" | "Infinity" | "-Infinity";
19
19
  } | {
20
20
  type: "double";
21
- value: number;
21
+ value: number | "NaN" | "Infinity" | "-Infinity";
22
22
  } | {
23
23
  type: "byteArray";
24
24
  value: number[];
@@ -3,7 +3,7 @@ import { createError, ERROR_CODES } from "./errors.js";
3
3
  const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
4
4
  const MALFORMED_WINDOWS_DRIVE_PATH = /^[A-Za-z]:(?![\\/])/;
5
5
  const WSL_MOUNT_PATH = /^\/mnt\/[a-z](?:\/|$)/i;
6
- const UNC_WSL_PATH = /^(?:\\\\wsl\$\\|\/\/wsl\$\/)/i;
6
+ const UNC_WSL_PATH = /^(?:\\\\(?:wsl\$|wsl\.localhost)\\|\/\/(?:wsl\$|wsl\.localhost)\/)/i;
7
7
  function normalizeToUnixSlashes(value) {
8
8
  return value.replace(/\\/g, "/");
9
9
  }
@@ -12,10 +12,11 @@ function normalizeToWindowsSlashes(value) {
12
12
  }
13
13
  function parseUncWslPath(pathValue) {
14
14
  const normalized = normalizeToUnixSlashes(pathValue);
15
- if (!normalized.toLowerCase().startsWith("//wsl$/")) {
15
+ const prefixMatch = normalized.match(/^\/\/(?:wsl\$|wsl\.localhost)\//i);
16
+ if (!prefixMatch) {
16
17
  return undefined;
17
18
  }
18
- const remainder = normalized.slice("//wsl$/".length).replace(/^\/+/, "");
19
+ const remainder = normalized.slice(prefixMatch[0].length).replace(/^\/+/, "");
19
20
  const slashIndex = remainder.indexOf("/");
20
21
  if (slashIndex < 0) {
21
22
  return {
@@ -99,6 +100,12 @@ function toWslPath(pathValue, runtime, field) {
99
100
  }
100
101
  const parsed = parseUncWslPath(pathValue);
101
102
  if (parsed) {
103
+ const distro = runtime.wslDistro?.trim();
104
+ if (!distro || parsed.distro.toLowerCase() !== distro.toLowerCase()) {
105
+ throwInvalidPath(pathValue, distro
106
+ ? `UNC WSL path targets distro "${parsed.distro}", but this server runs in distro "${distro}".`
107
+ : `Cannot resolve UNC WSL path for distro "${parsed.distro}" without a known WSL distro name.`, runtime, field);
108
+ }
102
109
  const rest = parsed.innerPath.replace(/^\/+/, "");
103
110
  return rest ? `/${rest}` : "/";
104
111
  }
@@ -28,9 +28,11 @@ export declare class RegistryService {
28
28
  private readonly config;
29
29
  private readonly versionService;
30
30
  private readonly registryCache;
31
+ private readonly loadLocks;
31
32
  constructor(config: Config, versionService: VersionService);
32
33
  getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
33
34
  private loadRegistries;
35
+ private loadRegistriesInternal;
34
36
  private loadExistingRegistries;
35
37
  private readRegistryFileOrThrow;
36
38
  private isInvalidRegistrySnapshot;
@@ -126,7 +126,7 @@ function runDataGen(serverJarPath, outputDir, version) {
126
126
  ];
127
127
  log("info", "registry.datagen.start", { version, isLegacy, serverJarPath, outputDir });
128
128
  const proc = spawn("java", args, {
129
- stdio: ["ignore", "pipe", "pipe"],
129
+ stdio: ["ignore", "ignore", "pipe"],
130
130
  cwd: outputDir
131
131
  });
132
132
  let stderr = "";
@@ -194,6 +194,7 @@ export class RegistryService {
194
194
  config;
195
195
  versionService;
196
196
  registryCache = new Map();
197
+ loadLocks = new Map();
197
198
  constructor(config, versionService) {
198
199
  this.config = config;
199
200
  this.versionService = versionService;
@@ -270,6 +271,20 @@ export class RegistryService {
270
271
  };
271
272
  }
272
273
  async loadRegistries(version, warnings) {
274
+ const existingLock = this.loadLocks.get(version);
275
+ if (existingLock) {
276
+ return existingLock;
277
+ }
278
+ const loadPromise = this.loadRegistriesInternal(version, warnings);
279
+ this.loadLocks.set(version, loadPromise);
280
+ try {
281
+ return await loadPromise;
282
+ }
283
+ finally {
284
+ this.loadLocks.delete(version);
285
+ }
286
+ }
287
+ async loadRegistriesInternal(version, warnings) {
273
288
  const cached = this.registryCache.get(version);
274
289
  if (cached) {
275
290
  // LRU touch: move to most-recently-used so the bound evicts the true
@@ -40,7 +40,6 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
40
40
  const timer = setTimeout(() => timeout.abort(), timeoutMs);
41
41
  try {
42
42
  const response = await fetchFn(url, { signal: timeout.signal });
43
- clearTimeout(timer);
44
43
  const status = response.status;
45
44
  if (status === 404) {
46
45
  return { ok: false, statusCode: status };
@@ -71,7 +70,7 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
71
70
  }
72
71
  else {
73
72
  const readable = Readable.fromWeb(response.body);
74
- await pipeline(readable, createWriteStream(tempPath));
73
+ await pipeline(readable, createWriteStream(tempPath), { signal: timeout.signal });
75
74
  }
76
75
  const contentLength = statSync(tempPath).size;
77
76
  renameSync(tempPath, destinationPath);
@@ -95,13 +94,15 @@ export async function downloadToCache(url, destinationPath, opts = {}) {
95
94
  }
96
95
  }
97
96
  catch (caughtError) {
98
- clearTimeout(timer);
99
97
  if (attempt >= maxRetries) {
100
98
  throw caughtError instanceof Error ? caughtError : new Error(String(caughtError));
101
99
  }
102
100
  await sleep(retryDelay(200, attempt));
103
101
  attempt += 1;
104
102
  }
103
+ finally {
104
+ clearTimeout(timer);
105
+ }
105
106
  }
106
107
  }
107
108
  export function defaultDownloadPath(cacheDir, url) {