@adhisang/minecraft-modding-mcp 4.2.0 → 5.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.
- package/CHANGELOG.md +40 -0
- package/README.md +6 -4
- package/dist/access-transformer-parser.d.ts +2 -1
- package/dist/access-transformer-parser.js +10 -1
- package/dist/build-suggested-call.js +31 -10
- package/dist/cache-registry.d.ts +8 -0
- package/dist/cache-registry.js +99 -6
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +24 -24
- package/dist/entry-tools/analyze-symbol-service.d.ts +57 -22
- package/dist/entry-tools/analyze-symbol-service.js +124 -28
- package/dist/entry-tools/batch-class-members-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-members-service.js +6 -6
- package/dist/entry-tools/batch-class-source-service.d.ts +3 -1
- package/dist/entry-tools/batch-class-source-service.js +5 -6
- package/dist/entry-tools/batch-mappings-service.d.ts +3 -1
- package/dist/entry-tools/batch-mappings-service.js +4 -5
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +3 -1
- package/dist/entry-tools/batch-symbol-exists-service.js +4 -5
- package/dist/entry-tools/compare-minecraft-service.d.ts +10 -10
- package/dist/entry-tools/entry-tool-schema.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +1 -3
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +2 -2
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +0 -5
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +0 -1
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +8 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +13 -2
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +1 -1
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +1 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +90 -89
- package/dist/entry-tools/inspect-minecraft/internal.js +30 -14
- package/dist/entry-tools/inspect-minecraft-service.d.ts +218 -218
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/entry-tools/response-contract.d.ts +1 -1
- package/dist/entry-tools/response-contract.js +1 -0
- package/dist/entry-tools/validate-project/cases/mixin.js +2 -1
- package/dist/entry-tools/validate-project/cases/project-summary.js +5 -3
- package/dist/entry-tools/validate-project-service.d.ts +42 -36
- package/dist/entry-tools/validate-project-service.js +3 -1
- package/dist/entry-tools/verify-mixin-target-service.d.ts +30 -0
- package/dist/entry-tools/verify-mixin-target-service.js +113 -22
- package/dist/error-mapping.d.ts +51 -0
- package/dist/error-mapping.js +157 -2
- package/dist/errors.js +4 -1
- package/dist/index.js +138 -71
- package/dist/java-process.d.ts +9 -0
- package/dist/java-process.js +11 -5
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/lookup.d.ts +1 -1
- package/dist/mapping/lookup.js +7 -1
- package/dist/mapping/types.d.ts +6 -0
- package/dist/mapping-service.d.ts +26 -2
- package/dist/mapping-service.js +178 -36
- package/dist/mcp-helpers.d.ts +1 -0
- package/dist/mcp-helpers.js +25 -3
- package/dist/minecraft-explorer-service.js +24 -11
- package/dist/mixin/access-validators.js +33 -1
- package/dist/mixin/annotation-validators.js +53 -7
- package/dist/mixin/parsed-validator.js +2 -2
- package/dist/mixin-parser.js +48 -4
- package/dist/mod-decompile-service.js +2 -1
- package/dist/mod-remap-service.d.ts +20 -1
- package/dist/mod-remap-service.js +95 -40
- package/dist/nbt/java-nbt-codec.js +87 -2
- package/dist/observability.d.ts +6 -0
- package/dist/observability.js +10 -0
- package/dist/page-cursor.d.ts +17 -0
- package/dist/page-cursor.js +0 -0
- package/dist/registry-service.js +25 -1
- package/dist/resources.js +45 -9
- package/dist/response-utils.d.ts +29 -4
- package/dist/response-utils.js +95 -5
- package/dist/source/access-validate.js +12 -2
- package/dist/source/artifact-resolver.js +1 -1
- package/dist/source/cache-metrics.js +2 -2
- package/dist/source/class-source/members-builder.d.ts +44 -1
- package/dist/source/class-source/members-builder.js +51 -4
- package/dist/source/class-source/snippet-builder.d.ts +13 -0
- package/dist/source/class-source/snippet-builder.js +48 -4
- package/dist/source/class-source-helpers.js +67 -11
- package/dist/source/class-source.js +97 -20
- package/dist/source/indexer.d.ts +31 -6
- package/dist/source/indexer.js +95 -25
- package/dist/source/search.d.ts +1 -1
- package/dist/source/search.js +94 -22
- package/dist/source/validate-mixin.d.ts +2 -1
- package/dist/source/validate-mixin.js +45 -4
- package/dist/source/workspace-target.js +15 -9
- package/dist/source-jar-reader.d.ts +54 -1
- package/dist/source-jar-reader.js +121 -10
- package/dist/source-service.d.ts +28 -7
- package/dist/storage/files-repo.d.ts +12 -0
- package/dist/storage/files-repo.js +24 -0
- package/dist/storage/symbols-repo.d.ts +6 -0
- package/dist/storage/symbols-repo.js +6 -0
- package/dist/symbols/symbol-extractor.js +35 -6
- package/dist/text-truncate.d.ts +7 -0
- package/dist/text-truncate.js +21 -0
- package/dist/tiny-remapper-resolver.js +10 -0
- package/dist/tiny-remapper-service.js +3 -3
- package/dist/tool-contract-manifest.js +2 -2
- package/dist/tool-execution-gate.js +12 -1
- package/dist/tool-guidance.d.ts +4 -1
- package/dist/tool-guidance.js +37 -12
- package/dist/tool-schemas.d.ts +559 -494
- package/dist/tool-schemas.js +99 -63
- package/dist/types.d.ts +1 -0
- package/dist/version-diff-service.js +5 -0
- package/dist/version-service.d.ts +3 -0
- package/dist/version-service.js +18 -2
- package/dist/warning-details.d.ts +18 -0
- package/dist/warning-details.js +63 -0
- package/docs/README-ja.md +2 -2
- package/docs/examples.md +26 -2
- package/docs/tool-reference.md +23 -17
- package/package.json +1 -1
|
@@ -43,13 +43,59 @@ export function validateInjection(inj, targetMembers, targetNames, issues, resol
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
else {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
// Name matched. If the reference carries a JVM descriptor, require an
|
|
47
|
+
// overload to match it; otherwise a wrong-signature target (e.g. tick(D)V
|
|
48
|
+
// when only tick(I)V exists) is silently reported as resolved.
|
|
49
|
+
const refDescriptor = extractMethodDescriptor(inj.method);
|
|
50
|
+
const sameNameMembers = [...members.constructors, ...members.methods].filter((member) => member.name === methodName);
|
|
51
|
+
if (refDescriptor &&
|
|
52
|
+
sameNameMembers.length > 0 &&
|
|
53
|
+
!sameNameMembers.some((member) => member.jvmDescriptor === refDescriptor)) {
|
|
54
|
+
const available = [...new Set(sameNameMembers.map((member) => member.jvmDescriptor))];
|
|
55
|
+
const isRemapFailed = remapFailedMembers?.get(targetName)?.has(methodName);
|
|
56
|
+
const isSigFailed = signatureFailedTargets?.has(targetName);
|
|
57
|
+
// Only a healthy mapping lets us trust that the user-written descriptor is
|
|
58
|
+
// byte-equal to the runtime descriptor; otherwise downgrade to a warning.
|
|
59
|
+
const mappingDegraded = Boolean(isRemapFailed) || Boolean(isSigFailed) || healthReport?.overallHealthy === false;
|
|
60
|
+
const resolutionPath = isRemapFailed
|
|
61
|
+
? "member-remap-failed"
|
|
62
|
+
: isSigFailed
|
|
63
|
+
? "source-signature-unavailable"
|
|
64
|
+
: undefined;
|
|
65
|
+
const issueConfidence = mappingDegraded
|
|
66
|
+
? "uncertain"
|
|
67
|
+
: confidence ?? "definite";
|
|
68
|
+
issues.push({
|
|
69
|
+
severity: mappingDegraded ? "warning" : "error",
|
|
70
|
+
kind: "descriptor-mismatch",
|
|
71
|
+
annotation: `@${inj.annotation}`,
|
|
72
|
+
target: `${targetName}#${inj.method}`,
|
|
73
|
+
message: `Method "${methodName}" exists in "${targetName}" but no overload matches descriptor ${refDescriptor} (available: ${available.join(", ")}).${mappingDegraded ? " (mapping degraded; may be a remap artifact)" : ""}`,
|
|
74
|
+
suggestions: available.length > 0 ? available : undefined,
|
|
75
|
+
line: inj.line,
|
|
76
|
+
confidence: issueConfidence,
|
|
77
|
+
confidenceReason: mappingDegraded
|
|
78
|
+
? `Member remap/signature degraded; descriptor mismatch may be a tooling artifact, not a true signature error.`
|
|
79
|
+
: confidenceReason,
|
|
80
|
+
resolutionPath,
|
|
81
|
+
falsePositiveRisk: computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence)
|
|
82
|
+
});
|
|
83
|
+
resolvedMembers.push({
|
|
84
|
+
annotation: `@${inj.annotation}`,
|
|
85
|
+
name: methodName,
|
|
86
|
+
line: inj.line,
|
|
87
|
+
status: "not-found"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
resolvedMembers.push({
|
|
92
|
+
annotation: `@${inj.annotation}`,
|
|
93
|
+
name: methodName,
|
|
94
|
+
line: inj.line,
|
|
95
|
+
resolvedTo: `${targetName}#${methodName}`,
|
|
96
|
+
status: "resolved"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
53
99
|
}
|
|
54
100
|
}
|
|
55
101
|
}
|
|
@@ -209,7 +209,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
209
209
|
if (version) {
|
|
210
210
|
assignSuggested(issue, "get-class-source", {
|
|
211
211
|
className: issue.target,
|
|
212
|
-
target: {
|
|
212
|
+
target: { kind: "version", value: version },
|
|
213
213
|
...(mapping ? { mapping } : {}),
|
|
214
214
|
mode: "metadata",
|
|
215
215
|
...classSourceContext
|
|
@@ -236,7 +236,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
236
236
|
if (version) {
|
|
237
237
|
assignSuggested(issue, "get-class-source", {
|
|
238
238
|
className,
|
|
239
|
-
target: {
|
|
239
|
+
target: { kind: "version", value: version },
|
|
240
240
|
...(mapping ? { mapping } : {}),
|
|
241
241
|
mode: "metadata",
|
|
242
242
|
...classSourceContext
|
package/dist/mixin-parser.js
CHANGED
|
@@ -61,9 +61,38 @@ function collectMultilineAnnotation(lines, startIndex) {
|
|
|
61
61
|
* Lines with inline annotation + declaration (e.g. `@Nullable private int x;`)
|
|
62
62
|
* are treated as declaration lines (not skipped).
|
|
63
63
|
*/
|
|
64
|
+
function isUpperCaseLetter(ch) {
|
|
65
|
+
return ch !== ch.toLowerCase() && ch === ch.toUpperCase();
|
|
66
|
+
}
|
|
64
67
|
function skipAnnotations(lines, startIndex) {
|
|
65
68
|
let idx = startIndex;
|
|
66
|
-
while (idx < lines.length
|
|
69
|
+
while (idx < lines.length) {
|
|
70
|
+
const current = lines[idx];
|
|
71
|
+
// Skip blank lines and comment-only lines that sit between an annotation and the
|
|
72
|
+
// member declaration; otherwise the declaration line is missed and the member dropped.
|
|
73
|
+
if (/^\s*$/.test(current) || /^\s*\/\//.test(current)) {
|
|
74
|
+
idx++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (/^\s*\/\*/.test(current)) {
|
|
78
|
+
if (current.includes("*/")) {
|
|
79
|
+
const afterComment = current.slice(current.indexOf("*/") + 2).trim();
|
|
80
|
+
if (afterComment === "") {
|
|
81
|
+
idx++;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
break; // declaration text follows an inline block comment
|
|
85
|
+
}
|
|
86
|
+
idx++;
|
|
87
|
+
while (idx < lines.length && !lines[idx].includes("*/")) {
|
|
88
|
+
idx++;
|
|
89
|
+
}
|
|
90
|
+
idx++; // move past the closing "*/" line
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!/^\s*@/.test(current)) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
67
96
|
if (lines[idx].includes("(")) {
|
|
68
97
|
// Check if parentheses are unbalanced (multi-line annotation) — always skip
|
|
69
98
|
let depth = 0;
|
|
@@ -106,18 +135,33 @@ const INLINE_ANNOTATION_RE = /\s*@[\w$.]+(?:\([^)]*\))?\s*/g;
|
|
|
106
135
|
function stripInlineAnnotations(line) {
|
|
107
136
|
return line.replace(INLINE_ANNOTATION_RE, " ").trim();
|
|
108
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* JavaBeans `Introspector.decapitalize`, which SpongePowered Mixin's AccessorInfo
|
|
140
|
+
* uses for inflection: leave the name unchanged when its first two characters are
|
|
141
|
+
* both upper case (getURL -> URL, getNBT -> NBT), otherwise lower-case the first
|
|
142
|
+
* character (getId -> id, getHealth -> health).
|
|
143
|
+
*/
|
|
144
|
+
function decapitalize(name) {
|
|
145
|
+
if (name.length === 0) {
|
|
146
|
+
return name;
|
|
147
|
+
}
|
|
148
|
+
if (name.length > 1 && isUpperCaseLetter(name.charAt(1)) && isUpperCaseLetter(name.charAt(0))) {
|
|
149
|
+
return name;
|
|
150
|
+
}
|
|
151
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
152
|
+
}
|
|
109
153
|
function inferAccessorTarget(methodName) {
|
|
110
154
|
const getterMatch = GETTER_PREFIX_RE.exec(methodName);
|
|
111
155
|
if (getterMatch) {
|
|
112
|
-
return
|
|
156
|
+
return decapitalize(getterMatch[1]);
|
|
113
157
|
}
|
|
114
158
|
const setterMatch = SETTER_PREFIX_RE.exec(methodName);
|
|
115
159
|
if (setterMatch) {
|
|
116
|
-
return
|
|
160
|
+
return decapitalize(setterMatch[1]);
|
|
117
161
|
}
|
|
118
162
|
const invokerMatch = INVOKER_PREFIX_RE.exec(methodName);
|
|
119
163
|
if (invokerMatch) {
|
|
120
|
-
return
|
|
164
|
+
return decapitalize(invokerMatch[1]);
|
|
121
165
|
}
|
|
122
166
|
return methodName;
|
|
123
167
|
}
|
|
@@ -7,6 +7,7 @@ import { decompileBinaryJar } from "./decompiler/vineflower.js";
|
|
|
7
7
|
import { resolveVineflowerJar } from "./vineflower-resolver.js";
|
|
8
8
|
import { analyzeModJar } from "./mod-analyzer.js";
|
|
9
9
|
import { validateAndNormalizeJarPath } from "./path-resolver.js";
|
|
10
|
+
import { sliceToMaxCharsSafe } from "./text-truncate.js";
|
|
10
11
|
const DECOMPILE_TIMEOUT_MS = 300_000;
|
|
11
12
|
function modDecompileCacheKey(jarPath) {
|
|
12
13
|
return createHash("sha256").update(jarPath).digest("hex");
|
|
@@ -122,7 +123,7 @@ export class ModDecompileService {
|
|
|
122
123
|
}
|
|
123
124
|
// Apply maxChars truncation
|
|
124
125
|
if (input.maxChars != null && input.maxChars > 0 && content.length > input.maxChars) {
|
|
125
|
-
content = content
|
|
126
|
+
content = sliceToMaxCharsSafe(content, input.maxChars);
|
|
126
127
|
charsTruncated = true;
|
|
127
128
|
truncated = true;
|
|
128
129
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
import { resolveTinyMappingFile } from "./mapping-service.js";
|
|
2
|
+
import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
|
|
3
|
+
import { remapJar } from "./tiny-remapper-service.js";
|
|
4
|
+
import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
|
|
1
5
|
import type { Config } from "./types.js";
|
|
2
6
|
export interface ModRemapInput {
|
|
3
7
|
inputJar: string;
|
|
4
8
|
outputJar?: string;
|
|
5
9
|
mcVersion?: string;
|
|
6
10
|
targetMapping: "yarn" | "mojang";
|
|
11
|
+
/**
|
|
12
|
+
* Skip the remap cache and force re-resolution of the newest yarn build
|
|
13
|
+
* (bypasses both the output-jar cache hit and the yarn metadata TTL).
|
|
14
|
+
*/
|
|
15
|
+
forceRemap?: boolean;
|
|
7
16
|
}
|
|
8
17
|
export interface ModRemapResult {
|
|
9
18
|
outputJar: string;
|
|
@@ -14,4 +23,14 @@ export interface ModRemapResult {
|
|
|
14
23
|
durationMs: number;
|
|
15
24
|
warnings: string[];
|
|
16
25
|
}
|
|
17
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Injectable external dependencies, used by tests to drive the remap pipeline
|
|
28
|
+
* without spawning Java or downloading mapping jars. Production callers omit it.
|
|
29
|
+
*/
|
|
30
|
+
export interface RemapModJarDeps {
|
|
31
|
+
remapJar?: typeof remapJar;
|
|
32
|
+
resolveTinyMappingFile?: typeof resolveTinyMappingFile;
|
|
33
|
+
resolveMojangTinyFile?: typeof resolveMojangTinyFile;
|
|
34
|
+
resolveTinyRemapperJar?: typeof resolveTinyRemapperJar;
|
|
35
|
+
}
|
|
36
|
+
export declare function remapModJar(input: ModRemapInput, config: Config, deps?: RemapModJarDeps): Promise<ModRemapResult>;
|
|
@@ -11,6 +11,13 @@ import { normalizePathForHost } from "./path-converter.js";
|
|
|
11
11
|
import { detectFabricLikeInputNamespace } from "./source-jar-reader.js";
|
|
12
12
|
import { remapJar } from "./tiny-remapper-service.js";
|
|
13
13
|
import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
|
|
14
|
+
// Bumped whenever the remap pipeline changes in a way that makes previously
|
|
15
|
+
// cached output jars wrong. The v1 pipeline mis-mapped intermediary jars to
|
|
16
|
+
// mojang using an obfuscated<->mojang tiny (no intermediary namespace), so any
|
|
17
|
+
// cached mojang output is invalid and must not be reused. v3 adds the resolved
|
|
18
|
+
// mapping identity (yarn build coordinate) to the cache key so a newer yarn
|
|
19
|
+
// build no longer serves the stale jar; the bump invalidates v2 outputs once.
|
|
20
|
+
const REMAP_PIPELINE_VERSION = "v3";
|
|
14
21
|
function normalizeTargetNamespace(target) {
|
|
15
22
|
return target === "yarn" ? "yarn" : "mojang";
|
|
16
23
|
}
|
|
@@ -58,14 +65,18 @@ function copyJarToDestination(sourceJar, destinationJar) {
|
|
|
58
65
|
mkdirSync(dirname(destinationJar), { recursive: true });
|
|
59
66
|
copyFileSync(sourceJar, destinationJar);
|
|
60
67
|
}
|
|
61
|
-
function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion) {
|
|
68
|
+
function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion, mappingIdentity) {
|
|
62
69
|
const stat = statSync(inputJar, { throwIfNoEntry: false });
|
|
63
70
|
const signature = stat ? `${stat.mtimeMs}:${stat.size}` : "unknown";
|
|
64
71
|
return createHash("sha256")
|
|
65
|
-
.update(`${inputJar}|${signature}|${fromNamespace}|${targetNamespace}|${mcVersion}`)
|
|
72
|
+
.update(`${REMAP_PIPELINE_VERSION}|${inputJar}|${signature}|${fromNamespace}|${targetNamespace}|${mcVersion}|${mappingIdentity}`)
|
|
66
73
|
.digest("hex");
|
|
67
74
|
}
|
|
68
|
-
export async function remapModJar(input, config) {
|
|
75
|
+
export async function remapModJar(input, config, deps = {}) {
|
|
76
|
+
const remapJarFn = deps.remapJar ?? remapJar;
|
|
77
|
+
const resolveTinyMappingFileFn = deps.resolveTinyMappingFile ?? resolveTinyMappingFile;
|
|
78
|
+
const resolveMojangTinyFileFn = deps.resolveMojangTinyFile ?? resolveMojangTinyFile;
|
|
79
|
+
const resolveTinyRemapperJarFn = deps.resolveTinyRemapperJar ?? resolveTinyRemapperJar;
|
|
69
80
|
const startedAt = Date.now();
|
|
70
81
|
const warnings = [];
|
|
71
82
|
// 1. Normalize input JAR path
|
|
@@ -111,12 +122,33 @@ export async function remapModJar(input, config) {
|
|
|
111
122
|
});
|
|
112
123
|
}
|
|
113
124
|
const outputJar = resolveOutputJarPath(input, normalizedInput, analysis.modId, analysis.modVersion);
|
|
114
|
-
// 4.
|
|
115
|
-
|
|
125
|
+
// 4. Resolve the mapping identity BEFORE the cache key so the key reflects
|
|
126
|
+
// which mappings produced the jar. Yarn builds are mutable (Fabric publishes
|
|
127
|
+
// new builds per MC version), so the resolved yarn coordinate is the identity;
|
|
128
|
+
// mojang is immutable per release so its version-keyed path string suffices.
|
|
129
|
+
// The yarn resolution is TTL-fenced (see resolveTinyMappingFile), so on the
|
|
130
|
+
// common warm path this is zero-network and stays off the cache-hit fast path.
|
|
131
|
+
let mappingIdentity;
|
|
132
|
+
let resolvedYarnTiny;
|
|
133
|
+
if (fromNamespace === resolvedTargetNamespace) {
|
|
134
|
+
// No remap will run (input already in target namespace); the output is a
|
|
135
|
+
// copy of the input, so no mapping identity is involved.
|
|
136
|
+
mappingIdentity = `copy:${resolvedTargetNamespace}`;
|
|
137
|
+
}
|
|
138
|
+
else if (resolvedTargetNamespace === "yarn") {
|
|
139
|
+
resolvedYarnTiny = await resolveTinyMappingFileFn(mcVersion, "yarn", config.cacheDir, undefined, {
|
|
140
|
+
forceRefresh: input.forceRemap
|
|
141
|
+
});
|
|
142
|
+
mappingIdentity = resolvedYarnTiny.coordinate ?? resolvedYarnTiny.path;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
mappingIdentity = `${mcVersion}-mojang-merged.tiny`;
|
|
146
|
+
}
|
|
147
|
+
const cacheKey = buildCacheKey(normalizedInput, fromNamespace, resolvedTargetNamespace, mcVersion, mappingIdentity);
|
|
116
148
|
const cacheDir = join(config.cacheDir, "remapped-mods");
|
|
117
149
|
mkdirSync(cacheDir, { recursive: true });
|
|
118
150
|
const cachedOutput = join(cacheDir, `${cacheKey}.jar`);
|
|
119
|
-
if (existsSync(cachedOutput)) {
|
|
151
|
+
if (!input.forceRemap && existsSync(cachedOutput)) {
|
|
120
152
|
const cacheHitOutputJar = input.outputJar
|
|
121
153
|
? outputJar
|
|
122
154
|
: cachedOutput;
|
|
@@ -161,51 +193,74 @@ export async function remapModJar(input, config) {
|
|
|
161
193
|
});
|
|
162
194
|
}
|
|
163
195
|
// 5. Resolve tiny-remapper
|
|
164
|
-
const tinyRemapperJar = await
|
|
165
|
-
|
|
166
|
-
let mappingsFile;
|
|
167
|
-
let toNamespace;
|
|
196
|
+
const tinyRemapperJar = await resolveTinyRemapperJarFn(config.cacheDir, config.tinyRemapperJarPath);
|
|
197
|
+
const passes = [];
|
|
168
198
|
if (resolvedTargetNamespace === "yarn") {
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
// The Fabric yarn v2 tiny declares both intermediary and named, so a single
|
|
200
|
+
// intermediary -> named pass is sufficient.
|
|
201
|
+
// Reuse the coordinate resolved for the cache key (already TTL-fenced).
|
|
202
|
+
const yarnTiny = resolvedYarnTiny ?? await resolveTinyMappingFileFn(mcVersion, "yarn", config.cacheDir, undefined, {
|
|
203
|
+
forceRefresh: input.forceRemap
|
|
204
|
+
});
|
|
205
|
+
passes.push({ mappingsFile: yarnTiny.path, fromNamespace, toNamespace: "named" });
|
|
171
206
|
}
|
|
172
207
|
else {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
// Mojang target. The merged mojang tiny only bridges obfuscated <-> mojang,
|
|
209
|
+
// but a Fabric/Quilt jar is intermediary-named, so a direct intermediary ->
|
|
210
|
+
// mojang remap finds no matching keys and silently no-ops. Remap in two passes:
|
|
211
|
+
// pass 1: intermediary -> official (= obfuscated) via the Fabric intermediary v2 tiny
|
|
212
|
+
// pass 2: obfuscated -> mojang via the merged mojang tiny
|
|
213
|
+
const mojangTiny = await resolveMojangTinyFileFn(mcVersion, config);
|
|
176
214
|
warnings.push(...mojangTiny.warnings);
|
|
215
|
+
if (fromNamespace === "intermediary") {
|
|
216
|
+
const intermediaryTiny = await resolveTinyMappingFileFn(mcVersion, "intermediary", config.cacheDir);
|
|
217
|
+
passes.push({ mappingsFile: intermediaryTiny.path, fromNamespace: "intermediary", toNamespace: "official" });
|
|
218
|
+
passes.push({ mappingsFile: mojangTiny.path, fromNamespace: "obfuscated", toNamespace: "mojang" });
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Defensive: an already-obfuscated input maps directly. (mojang -> mojang is
|
|
222
|
+
// handled earlier by the copy short-circuit, so this is not normally reached.)
|
|
223
|
+
passes.push({ mappingsFile: mojangTiny.path, fromNamespace: "obfuscated", toNamespace: "mojang" });
|
|
224
|
+
}
|
|
177
225
|
}
|
|
178
226
|
mkdirSync(dirname(outputJar), { recursive: true });
|
|
179
|
-
//
|
|
227
|
+
// 7. Use a temporary directory for intermediate work
|
|
180
228
|
const tempDir = join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}`);
|
|
181
229
|
mkdirSync(tempDir, { recursive: true });
|
|
182
230
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
throw createError({
|
|
198
|
-
code: caughtError.code,
|
|
199
|
-
message: caughtError.message,
|
|
200
|
-
details: {
|
|
201
|
-
...(caughtError.details ?? {}),
|
|
202
|
-
fromMapping: fromNamespace,
|
|
203
|
-
targetMapping: input.targetMapping,
|
|
204
|
-
resolvedTargetNamespace
|
|
205
|
-
}
|
|
231
|
+
let passInput = normalizedInput;
|
|
232
|
+
let tempOutput = "";
|
|
233
|
+
for (let passIndex = 0; passIndex < passes.length; passIndex += 1) {
|
|
234
|
+
const pass = passes[passIndex];
|
|
235
|
+
tempOutput = join(tempDir, `pass-${passIndex}.jar`);
|
|
236
|
+
try {
|
|
237
|
+
await remapJarFn(tinyRemapperJar, {
|
|
238
|
+
inputJar: passInput,
|
|
239
|
+
outputJar: tempOutput,
|
|
240
|
+
mappingsFile: pass.mappingsFile,
|
|
241
|
+
fromNamespace: pass.fromNamespace,
|
|
242
|
+
toNamespace: pass.toNamespace,
|
|
243
|
+
timeoutMs: config.remapTimeoutMs,
|
|
244
|
+
maxMemoryMb: config.remapMaxMemoryMb
|
|
206
245
|
});
|
|
207
246
|
}
|
|
208
|
-
|
|
247
|
+
catch (caughtError) {
|
|
248
|
+
if (isAppError(caughtError)) {
|
|
249
|
+
throw createError({
|
|
250
|
+
code: caughtError.code,
|
|
251
|
+
message: caughtError.message,
|
|
252
|
+
details: {
|
|
253
|
+
...(caughtError.details ?? {}),
|
|
254
|
+
fromMapping: fromNamespace,
|
|
255
|
+
targetMapping: input.targetMapping,
|
|
256
|
+
resolvedTargetNamespace,
|
|
257
|
+
pass: `${pass.fromNamespace}->${pass.toNamespace}`
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
throw caughtError;
|
|
262
|
+
}
|
|
263
|
+
passInput = tempOutput;
|
|
209
264
|
}
|
|
210
265
|
// Copy to final destination and cache
|
|
211
266
|
copyFileSync(tempOutput, outputJar);
|
|
@@ -37,6 +37,91 @@ function resolveTagName(tagId, pointer) {
|
|
|
37
37
|
}
|
|
38
38
|
return tagName;
|
|
39
39
|
}
|
|
40
|
+
// Java NBT TAG_String uses MUTF-8 (Mojang's on-disk format matches the JVM
|
|
41
|
+
// `DataInput/OutputStream.{read,write}UTF` contract), which differs from UTF-8
|
|
42
|
+
// for two ranges: `U+0000` encodes as `C0 80`, and supplementary code points
|
|
43
|
+
// (`U+10000`+) encode as two 3-byte sequences for the UTF-16 surrogate pair
|
|
44
|
+
// (6 bytes total). All other characters share the UTF-8 byte sequence.
|
|
45
|
+
function mutf8ByteLength(value) {
|
|
46
|
+
let length = 0;
|
|
47
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
48
|
+
const c = value.charCodeAt(i);
|
|
49
|
+
if (c === 0x0000) {
|
|
50
|
+
length += 2;
|
|
51
|
+
}
|
|
52
|
+
else if (c <= 0x007f) {
|
|
53
|
+
length += 1;
|
|
54
|
+
}
|
|
55
|
+
else if (c <= 0x07ff) {
|
|
56
|
+
length += 2;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
length += 3;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return length;
|
|
63
|
+
}
|
|
64
|
+
function encodeMutf8(value) {
|
|
65
|
+
const buffer = Buffer.allocUnsafe(mutf8ByteLength(value));
|
|
66
|
+
let offset = 0;
|
|
67
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
68
|
+
const c = value.charCodeAt(i);
|
|
69
|
+
if (c === 0x0000) {
|
|
70
|
+
buffer[offset++] = 0xc0;
|
|
71
|
+
buffer[offset++] = 0x80;
|
|
72
|
+
}
|
|
73
|
+
else if (c <= 0x007f) {
|
|
74
|
+
buffer[offset++] = c;
|
|
75
|
+
}
|
|
76
|
+
else if (c <= 0x07ff) {
|
|
77
|
+
buffer[offset++] = 0xc0 | (c >> 6);
|
|
78
|
+
buffer[offset++] = 0x80 | (c & 0x3f);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
buffer[offset++] = 0xe0 | (c >> 12);
|
|
82
|
+
buffer[offset++] = 0x80 | ((c >> 6) & 0x3f);
|
|
83
|
+
buffer[offset++] = 0x80 | (c & 0x3f);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return buffer;
|
|
87
|
+
}
|
|
88
|
+
function decodeMutf8(buffer, start, end) {
|
|
89
|
+
const codeUnits = [];
|
|
90
|
+
let i = start;
|
|
91
|
+
while (i < end) {
|
|
92
|
+
const b0 = buffer[i++];
|
|
93
|
+
if ((b0 & 0x80) === 0) {
|
|
94
|
+
codeUnits.push(b0);
|
|
95
|
+
}
|
|
96
|
+
else if ((b0 & 0xe0) === 0xc0) {
|
|
97
|
+
if (i >= end) {
|
|
98
|
+
throw parseError("Truncated MUTF-8 2-byte sequence.", { offset: i - 1 });
|
|
99
|
+
}
|
|
100
|
+
const b1 = buffer[i++];
|
|
101
|
+
if ((b1 & 0xc0) !== 0x80) {
|
|
102
|
+
throw parseError("Invalid MUTF-8 continuation byte.", { byte: b1, offset: i - 1 });
|
|
103
|
+
}
|
|
104
|
+
codeUnits.push(((b0 & 0x1f) << 6) | (b1 & 0x3f));
|
|
105
|
+
}
|
|
106
|
+
else if ((b0 & 0xf0) === 0xe0) {
|
|
107
|
+
if (i + 1 >= end) {
|
|
108
|
+
throw parseError("Truncated MUTF-8 3-byte sequence.", { offset: i - 1 });
|
|
109
|
+
}
|
|
110
|
+
const b1 = buffer[i++];
|
|
111
|
+
const b2 = buffer[i++];
|
|
112
|
+
if ((b1 & 0xc0) !== 0x80 || (b2 & 0xc0) !== 0x80) {
|
|
113
|
+
throw parseError("Invalid MUTF-8 continuation byte.", { offset: i - 2 });
|
|
114
|
+
}
|
|
115
|
+
codeUnits.push(((b0 & 0x0f) << 12) | ((b1 & 0x3f) << 6) | (b2 & 0x3f));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw parseError("Invalid MUTF-8 lead byte.", { byte: b0, offset: i - 1 });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// NBT TAG_String is bounded by uint16 length so codeUnits.length <= 65535,
|
|
122
|
+
// safely within String.fromCharCode's argument cap.
|
|
123
|
+
return String.fromCharCode(...codeUnits);
|
|
124
|
+
}
|
|
40
125
|
class NbtReader {
|
|
41
126
|
buffer;
|
|
42
127
|
offset = 0;
|
|
@@ -103,7 +188,7 @@ class NbtReader {
|
|
|
103
188
|
readString() {
|
|
104
189
|
const byteLength = this.readUInt16();
|
|
105
190
|
this.ensure(byteLength);
|
|
106
|
-
const value = this.buffer
|
|
191
|
+
const value = decodeMutf8(this.buffer, this.offset, this.offset + byteLength);
|
|
107
192
|
this.offset += byteLength;
|
|
108
193
|
return value;
|
|
109
194
|
}
|
|
@@ -154,7 +239,7 @@ class NbtWriter {
|
|
|
154
239
|
this.chunks.push(chunk);
|
|
155
240
|
}
|
|
156
241
|
writeString(value) {
|
|
157
|
-
const encoded =
|
|
242
|
+
const encoded = encodeMutf8(value);
|
|
158
243
|
if (encoded.length > 0xffff) {
|
|
159
244
|
throw encodeError("NBT string length exceeds uint16.", { byteLength: encoded.length });
|
|
160
245
|
}
|
package/dist/observability.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface RuntimeMetricSnapshot {
|
|
|
40
40
|
search_rows_scanned: number;
|
|
41
41
|
search_rows_returned: number;
|
|
42
42
|
search_indexed_zero_shortcircuit_count: number;
|
|
43
|
+
search_like_prefilter_count: number;
|
|
44
|
+
search_scan_truncated_count: number;
|
|
43
45
|
reindex_count: number;
|
|
44
46
|
reindex_skip_count: number;
|
|
45
47
|
cache_evictions: number;
|
|
@@ -74,6 +76,8 @@ export declare class RuntimeMetrics {
|
|
|
74
76
|
private searchRowsScanned;
|
|
75
77
|
private searchRowsReturned;
|
|
76
78
|
private searchIndexedZeroShortcircuitCount;
|
|
79
|
+
private searchLikePrefilterCount;
|
|
80
|
+
private searchScanTruncatedCount;
|
|
77
81
|
private reindexCount;
|
|
78
82
|
private reindexSkipCount;
|
|
79
83
|
private cacheEvictions;
|
|
@@ -102,6 +106,8 @@ export declare class RuntimeMetrics {
|
|
|
102
106
|
recordSearchRowsScanned(count: number): void;
|
|
103
107
|
recordSearchRowsReturned(count: number): void;
|
|
104
108
|
recordSearchIndexedZeroShortcircuit(): void;
|
|
109
|
+
recordSearchLikePrefilter(): void;
|
|
110
|
+
recordSearchScanTruncated(): void;
|
|
105
111
|
recordReindex(): void;
|
|
106
112
|
recordReindexSkip(): void;
|
|
107
113
|
recordCacheEviction(count?: number): void;
|
package/dist/observability.js
CHANGED
|
@@ -26,6 +26,8 @@ export class RuntimeMetrics {
|
|
|
26
26
|
searchRowsScanned = 0;
|
|
27
27
|
searchRowsReturned = 0;
|
|
28
28
|
searchIndexedZeroShortcircuitCount = 0;
|
|
29
|
+
searchLikePrefilterCount = 0;
|
|
30
|
+
searchScanTruncatedCount = 0;
|
|
29
31
|
reindexCount = 0;
|
|
30
32
|
reindexSkipCount = 0;
|
|
31
33
|
cacheEvictions = 0;
|
|
@@ -128,6 +130,12 @@ export class RuntimeMetrics {
|
|
|
128
130
|
recordSearchIndexedZeroShortcircuit() {
|
|
129
131
|
this.searchIndexedZeroShortcircuitCount += 1;
|
|
130
132
|
}
|
|
133
|
+
recordSearchLikePrefilter() {
|
|
134
|
+
this.searchLikePrefilterCount += 1;
|
|
135
|
+
}
|
|
136
|
+
recordSearchScanTruncated() {
|
|
137
|
+
this.searchScanTruncatedCount += 1;
|
|
138
|
+
}
|
|
131
139
|
recordReindex() {
|
|
132
140
|
this.reindexCount += 1;
|
|
133
141
|
}
|
|
@@ -192,6 +200,8 @@ export class RuntimeMetrics {
|
|
|
192
200
|
search_rows_scanned: this.searchRowsScanned,
|
|
193
201
|
search_rows_returned: this.searchRowsReturned,
|
|
194
202
|
search_indexed_zero_shortcircuit_count: this.searchIndexedZeroShortcircuitCount,
|
|
203
|
+
search_like_prefilter_count: this.searchLikePrefilterCount,
|
|
204
|
+
search_scan_truncated_count: this.searchScanTruncatedCount,
|
|
195
205
|
reindex_count: this.reindexCount,
|
|
196
206
|
reindex_skip_count: this.reindexSkipCount,
|
|
197
207
|
cache_evictions: this.cacheEvictions,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type OffsetCursor = {
|
|
2
|
+
offset: number;
|
|
3
|
+
contextKey: string;
|
|
4
|
+
};
|
|
5
|
+
/** Build a stable context key from the query parameters a cursor belongs to. */
|
|
6
|
+
export declare function buildPageContextKey(parts: Array<string | number | boolean | undefined>): string;
|
|
7
|
+
export declare function encodeOffsetCursor(offset: number, contextKey: string): string;
|
|
8
|
+
export declare function decodeOffsetCursor(cursor: string | undefined): OffsetCursor | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the starting offset from an incoming cursor. Returns offset 0 when the
|
|
11
|
+
* cursor is absent, malformed, or belongs to a different query context, and
|
|
12
|
+
* reports whether a provided cursor was ignored.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveCursorOffset(cursor: string | undefined, contextKey: string): {
|
|
15
|
+
offset: number;
|
|
16
|
+
cursorIgnored: boolean;
|
|
17
|
+
};
|
|
Binary file
|
package/dist/registry-service.js
CHANGED
|
@@ -82,6 +82,25 @@ function parseRegistrySnapshot(raw, version, registryFile) {
|
|
|
82
82
|
details: { version, registryFile }
|
|
83
83
|
});
|
|
84
84
|
}
|
|
85
|
+
// Every registry value must carry an `entries` object. Without this guard a
|
|
86
|
+
// malformed value slips through and later throws a raw TypeError on `.entries`
|
|
87
|
+
// (registry-service.ts:268/305, version-diff-service.ts), bypassing the
|
|
88
|
+
// corrupt-snapshot regeneration path that keys off REGISTRY_GENERATION_FAILED.
|
|
89
|
+
for (const [registryName, value] of Object.entries(parsed)) {
|
|
90
|
+
const entries = value?.entries;
|
|
91
|
+
if (typeof value !== "object" ||
|
|
92
|
+
value === null ||
|
|
93
|
+
Array.isArray(value) ||
|
|
94
|
+
typeof entries !== "object" ||
|
|
95
|
+
entries === null ||
|
|
96
|
+
Array.isArray(entries)) {
|
|
97
|
+
throw createError({
|
|
98
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
99
|
+
message: `registries.json for version "${version}" has a malformed "${registryName}" registry (missing entries).`,
|
|
100
|
+
details: { version, registryFile, registryName }
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
85
104
|
return parsed;
|
|
86
105
|
}
|
|
87
106
|
function runDataGen(serverJarPath, outputDir, version) {
|
|
@@ -252,8 +271,13 @@ export class RegistryService {
|
|
|
252
271
|
}
|
|
253
272
|
async loadRegistries(version, warnings) {
|
|
254
273
|
const cached = this.registryCache.get(version);
|
|
255
|
-
if (cached)
|
|
274
|
+
if (cached) {
|
|
275
|
+
// LRU touch: move to most-recently-used so the bound evicts the true
|
|
276
|
+
// coldest entry (Map preserves insertion order).
|
|
277
|
+
this.registryCache.delete(version);
|
|
278
|
+
this.registryCache.set(version, cached);
|
|
256
279
|
return cached;
|
|
280
|
+
}
|
|
257
281
|
const registryDir = join(this.config.cacheDir, "registries", version);
|
|
258
282
|
const cachedRegistries = this.loadExistingRegistries(registryDir, version, warnings);
|
|
259
283
|
if (cachedRegistries) {
|