@adhisang/minecraft-modding-mcp 3.2.0 → 4.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.
- package/CHANGELOG.md +72 -0
- package/README.md +52 -32
- package/dist/build-suggested-call.d.ts +29 -0
- package/dist/build-suggested-call.js +58 -0
- package/dist/cache-registry.d.ts +3 -1
- package/dist/cache-registry.js +59 -7
- package/dist/config.d.ts +10 -1
- package/dist/config.js +52 -1
- package/dist/entry-tools/analyze-symbol-service.d.ts +18 -18
- package/dist/entry-tools/analyze-symbol-service.js +13 -2
- package/dist/entry-tools/batch-class-members-service.d.ts +34 -0
- package/dist/entry-tools/batch-class-members-service.js +97 -0
- package/dist/entry-tools/batch-class-source-service.d.ts +37 -0
- package/dist/entry-tools/batch-class-source-service.js +100 -0
- package/dist/entry-tools/batch-mappings-service.d.ts +36 -0
- package/dist/entry-tools/batch-mappings-service.js +66 -0
- package/dist/entry-tools/batch-runner.d.ts +72 -0
- package/dist/entry-tools/batch-runner.js +90 -0
- package/dist/entry-tools/batch-symbol-exists-service.d.ts +46 -0
- package/dist/entry-tools/batch-symbol-exists-service.js +113 -0
- package/dist/entry-tools/compare-minecraft-service.d.ts +6 -6
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/artifact.js +83 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.d.ts +6 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-members.js +80 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-overview.js +248 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/class-source.js +60 -0
- package/dist/entry-tools/inspect-minecraft/handlers/file.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/file.js +54 -0
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/list-files.js +100 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.d.ts +5 -0
- package/dist/entry-tools/inspect-minecraft/handlers/search.js +155 -0
- package/dist/entry-tools/inspect-minecraft/handlers/versions.d.ts +6 -0
- package/dist/entry-tools/inspect-minecraft/handlers/versions.js +49 -0
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +1042 -0
- package/dist/entry-tools/inspect-minecraft/internal.js +448 -0
- package/dist/entry-tools/inspect-minecraft-service.d.ts +213 -328
- package/dist/entry-tools/inspect-minecraft-service.js +20 -1238
- package/dist/entry-tools/manage-cache-service.d.ts +16 -16
- package/dist/entry-tools/validate-project/cases/access-transformer.d.ts +6 -0
- package/dist/entry-tools/validate-project/cases/access-transformer.js +106 -0
- package/dist/entry-tools/validate-project/cases/access-widener.d.ts +6 -0
- package/dist/entry-tools/validate-project/cases/access-widener.js +86 -0
- package/dist/entry-tools/validate-project/cases/mixin.d.ts +6 -0
- package/dist/entry-tools/validate-project/cases/mixin.js +90 -0
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +97 -0
- package/dist/entry-tools/validate-project/cases/project-summary.js +346 -0
- package/dist/entry-tools/validate-project/internal.d.ts +135 -0
- package/dist/entry-tools/validate-project/internal.js +287 -0
- package/dist/entry-tools/validate-project-service.d.ts +63 -47
- package/dist/entry-tools/validate-project-service.js +12 -482
- package/dist/entry-tools/verify-mixin-target-service.d.ts +133 -0
- package/dist/entry-tools/verify-mixin-target-service.js +323 -0
- package/dist/error-mapping.d.ts +40 -0
- package/dist/error-mapping.js +139 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +170 -1314
- package/dist/lru-list.d.ts +31 -0
- package/dist/lru-list.js +102 -0
- package/dist/mapping/internal-types.d.ts +54 -0
- package/dist/mapping/internal-types.js +14 -0
- package/dist/mapping/loaders/mojang.d.ts +2 -0
- package/dist/mapping/loaders/mojang.js +64 -0
- package/dist/mapping/loaders/tiny-loom.d.ts +2 -0
- package/dist/mapping/loaders/tiny-loom.js +73 -0
- package/dist/mapping/loaders/tiny-maven.d.ts +2 -0
- package/dist/mapping/loaders/tiny-maven.js +104 -0
- package/dist/mapping/loaders/types.d.ts +14 -0
- package/dist/mapping/loaders/types.js +2 -0
- package/dist/mapping/lookup.d.ts +52 -0
- package/dist/mapping/lookup.js +496 -0
- package/dist/mapping/parsers/normalize.d.ts +10 -0
- package/dist/mapping/parsers/normalize.js +52 -0
- package/dist/mapping/parsers/proguard.d.ts +20 -0
- package/dist/mapping/parsers/proguard.js +138 -0
- package/dist/mapping/parsers/symbol-records.d.ts +27 -0
- package/dist/mapping/parsers/symbol-records.js +216 -0
- package/dist/mapping/parsers/tiny.d.ts +9 -0
- package/dist/mapping/parsers/tiny.js +96 -0
- package/dist/mapping/types.d.ts +147 -0
- package/dist/mapping/types.js +2 -0
- package/dist/mapping-pipeline-service.d.ts +10 -1
- package/dist/mapping-pipeline-service.js +16 -3
- package/dist/mapping-service.d.ts +15 -144
- package/dist/mapping-service.js +179 -1119
- package/dist/mixin/access-validators.d.ts +9 -0
- package/dist/mixin/access-validators.js +257 -0
- package/dist/mixin/annotation-validators.d.ts +5 -0
- package/dist/mixin/annotation-validators.js +162 -0
- package/dist/mixin/helpers.d.ts +28 -0
- package/dist/mixin/helpers.js +315 -0
- package/dist/mixin/parsed-validator.d.ts +8 -0
- package/dist/mixin/parsed-validator.js +337 -0
- package/dist/mixin/types.d.ts +208 -0
- package/dist/mixin/types.js +28 -0
- package/dist/mixin-validator.d.ts +9 -201
- package/dist/mixin-validator.js +8 -1005
- package/dist/observability.d.ts +18 -1
- package/dist/observability.js +44 -1
- package/dist/response-utils.d.ts +44 -10
- package/dist/response-utils.js +131 -17
- package/dist/source/access-validate.d.ts +4 -0
- package/dist/source/access-validate.js +254 -0
- package/dist/source/artifact-resolver.d.ts +110 -0
- package/dist/source/artifact-resolver.js +1174 -0
- package/dist/source/cache-metrics.d.ts +26 -0
- package/dist/source/cache-metrics.js +172 -0
- package/dist/source/class-source/members-builder.d.ts +34 -0
- package/dist/source/class-source/members-builder.js +46 -0
- package/dist/source/class-source/snippet-builder.d.ts +19 -0
- package/dist/source/class-source/snippet-builder.js +46 -0
- package/dist/source/class-source-helpers.d.ts +34 -0
- package/dist/source/class-source-helpers.js +140 -0
- package/dist/source/class-source.d.ts +42 -0
- package/dist/source/class-source.js +883 -0
- package/dist/source/descriptor-utils.d.ts +6 -0
- package/dist/source/descriptor-utils.js +37 -0
- package/dist/source/file-access.d.ts +4 -0
- package/dist/source/file-access.js +102 -0
- package/dist/source/indexer.d.ts +82 -0
- package/dist/source/indexer.js +505 -0
- package/dist/source/lifecycle/diff-utils.d.ts +9 -0
- package/dist/source/lifecycle/diff-utils.js +107 -0
- package/dist/source/lifecycle/diff.d.ts +2 -0
- package/dist/source/lifecycle/diff.js +265 -0
- package/dist/source/lifecycle/mapping-helpers.d.ts +22 -0
- package/dist/source/lifecycle/mapping-helpers.js +327 -0
- package/dist/source/lifecycle/runtime-check.d.ts +2 -0
- package/dist/source/lifecycle/runtime-check.js +142 -0
- package/dist/source/lifecycle/trace.d.ts +2 -0
- package/dist/source/lifecycle/trace.js +231 -0
- package/dist/source/lifecycle.d.ts +4 -0
- package/dist/source/lifecycle.js +5 -0
- package/dist/source/search.d.ts +51 -0
- package/dist/source/search.js +676 -0
- package/dist/source/shared-utils.d.ts +6 -0
- package/dist/source/shared-utils.js +55 -0
- package/dist/source/state.d.ts +21 -0
- package/dist/source/state.js +19 -0
- package/dist/source/symbol-resolver.d.ts +3 -0
- package/dist/source/symbol-resolver.js +212 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.d.ts +3 -0
- package/dist/source/validate-mixin/pipeline/mapping-health.js +41 -0
- package/dist/source/validate-mixin/pipeline/parse.d.ts +2 -0
- package/dist/source/validate-mixin/pipeline/parse.js +10 -0
- package/dist/source/validate-mixin/pipeline/resolve.d.ts +3 -0
- package/dist/source/validate-mixin/pipeline/resolve.js +78 -0
- package/dist/source/validate-mixin/pipeline/target-lookup.d.ts +6 -0
- package/dist/source/validate-mixin/pipeline/target-lookup.js +260 -0
- package/dist/source/validate-mixin/pipeline-context.d.ts +72 -0
- package/dist/source/validate-mixin/pipeline-context.js +93 -0
- package/dist/source/validate-mixin.d.ts +22 -0
- package/dist/source/validate-mixin.js +799 -0
- package/dist/source/workspace-target.d.ts +18 -0
- package/dist/source/workspace-target.js +305 -0
- package/dist/source-resolver.d.ts +9 -1
- package/dist/source-resolver.js +14 -6
- package/dist/source-service.d.ts +178 -105
- package/dist/source-service.js +72 -5312
- package/dist/stage-emitter.d.ts +13 -0
- package/dist/stage-emitter.js +30 -0
- package/dist/stdio-supervisor.d.ts +61 -0
- package/dist/stdio-supervisor.js +326 -9
- package/dist/storage/artifacts-repo.d.ts +4 -1
- package/dist/storage/artifacts-repo.js +33 -5
- package/dist/storage/files-repo.d.ts +0 -2
- package/dist/storage/files-repo.js +0 -11
- package/dist/storage/migrations.d.ts +1 -1
- package/dist/storage/migrations.js +10 -2
- package/dist/storage/schema.d.ts +2 -0
- package/dist/storage/schema.js +25 -0
- package/dist/tool-contract-manifest.d.ts +1 -1
- package/dist/tool-contract-manifest.js +23 -6
- package/dist/tool-guidance.d.ts +82 -0
- package/dist/tool-guidance.js +734 -0
- package/dist/tool-schema-registry.d.ts +16 -0
- package/dist/tool-schema-registry.js +37 -0
- package/dist/tool-schemas.d.ts +3518 -0
- package/dist/tool-schemas.js +813 -0
- package/dist/types.d.ts +39 -0
- package/dist/version-service.js +7 -6
- package/dist/workspace-context-cache.d.ts +32 -0
- package/dist/workspace-context-cache.js +66 -0
- package/dist/workspace-mapping-service.d.ts +16 -0
- package/dist/workspace-mapping-service.js +173 -1
- package/docs/README-ja.md +414 -0
- package/docs/examples.md +483 -0
- package/docs/tool-reference.md +459 -0
- package/package.json +5 -2
package/dist/mapping-service.js
CHANGED
|
@@ -1,921 +1,43 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { mkdir,
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import
|
|
4
|
+
import { buildSuggestedCall } from "./build-suggested-call.js";
|
|
5
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
6
|
-
import { buildVersionSourceSearchRoots, normalizeOptionalProjectPath } from "./gradle-paths.js";
|
|
7
6
|
import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
|
|
8
7
|
import { collectMatchedJarEntriesAsUtf8 } from "./source-jar-reader.js";
|
|
9
8
|
import { VersionService, isUnobfuscatedVersion } from "./version-service.js";
|
|
9
|
+
import { buildSymbolKey, createClassSymbolRecord, mergeDirectionIndexes, normalizeMappedSymbolOutput } from "./mapping/parsers/symbol-records.js";
|
|
10
|
+
import { buildAdjacency, buildTargetRecordIndex, pairKey } from "./mapping/parsers/normalize.js";
|
|
11
|
+
import { MATCH_RANK, MAX_CANDIDATES } from "./mapping/internal-types.js";
|
|
12
|
+
import { applyDisambiguationHints, clampRowLimit, collectTargetRecords, effectiveLoomSearchProjectPath, inferAmbiguityReasons, invalidInputError, limitResolutionCandidates, lookupCandidates, mappingPriorityFromInput, mappingSourceOrder, namespacePath, normalizeIncludedKinds, normalizeQuerySymbol, pathToTransformChain, pathUsesSource, projectLookupCandidateDescriptor, requiresOnlyObfuscatedMojangGraph, toLookupCandidate, toResolutionCandidate } from "./mapping/lookup.js";
|
|
13
|
+
import { loadMojangPairs } from "./mapping/loaders/mojang.js";
|
|
14
|
+
import { loadTinyPairsFromLoom } from "./mapping/loaders/tiny-loom.js";
|
|
15
|
+
import { loadTinyPairsFromMaven } from "./mapping/loaders/tiny-maven.js";
|
|
10
16
|
const SUPPORTED_MAPPINGS = new Set([
|
|
11
17
|
"obfuscated",
|
|
12
18
|
"mojang",
|
|
13
19
|
"intermediary",
|
|
14
20
|
"yarn"
|
|
15
21
|
]);
|
|
16
|
-
|
|
17
|
-
exact: 3,
|
|
18
|
-
normalized: 2,
|
|
19
|
-
"simple-name": 1
|
|
20
|
-
};
|
|
21
|
-
const DESCRIPTOR_FALLBACK_CONFIDENCE = 0.85;
|
|
22
|
-
const MAX_CANDIDATES = 200;
|
|
23
|
-
const GLOB_SPECIAL_CHARS = /[\\!*+?()[\]{}@|]/g;
|
|
24
|
-
function createDirectionIndex() {
|
|
25
|
-
return {
|
|
26
|
-
exact: new Map(),
|
|
27
|
-
normalized: new Map(),
|
|
28
|
-
simple: new Map(),
|
|
29
|
-
records: new Map()
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function addToSetMap(map, key, value) {
|
|
33
|
-
const normalizedKey = key.trim();
|
|
34
|
-
if (!normalizedKey) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const existing = map.get(normalizedKey) ?? new Set();
|
|
38
|
-
existing.add(value);
|
|
39
|
-
map.set(normalizedKey, existing);
|
|
40
|
-
}
|
|
41
|
-
function normalizedVariants(symbol) {
|
|
42
|
-
const variants = [symbol];
|
|
43
|
-
let dotted;
|
|
44
|
-
if (symbol.includes("/")) {
|
|
45
|
-
dotted = symbol.replace(/\//g, ".");
|
|
46
|
-
if (dotted !== symbol) {
|
|
47
|
-
variants.push(dotted);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (symbol.includes(".")) {
|
|
51
|
-
const slashed = symbol.replace(/\./g, "/");
|
|
52
|
-
if (slashed !== symbol && slashed !== dotted) {
|
|
53
|
-
variants.push(slashed);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return variants;
|
|
57
|
-
}
|
|
58
|
-
function simpleName(symbol) {
|
|
59
|
-
const trimmed = symbol.trim();
|
|
60
|
-
if (!trimmed) {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
const withoutDescriptor = trimmed.includes("(") ? trimmed.slice(0, trimmed.indexOf("(")) : trimmed;
|
|
64
|
-
const base = withoutDescriptor.split(/[./]/).at(-1)?.trim();
|
|
65
|
-
return base || undefined;
|
|
66
|
-
}
|
|
67
|
-
function normalizeMappedSymbolOutput(symbol) {
|
|
68
|
-
return symbol.replace(/\//g, ".");
|
|
69
|
-
}
|
|
70
|
-
function splitOwnerAndName(symbol) {
|
|
71
|
-
const trimmed = symbol.trim();
|
|
72
|
-
const separatorIndex = Math.max(trimmed.lastIndexOf("."), trimmed.lastIndexOf("/"));
|
|
73
|
-
if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) {
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
owner: trimmed.slice(0, separatorIndex),
|
|
78
|
-
name: trimmed.slice(separatorIndex + 1)
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
function stripLineInfo(input) {
|
|
82
|
-
let value = input.trim();
|
|
83
|
-
while (/^\d+:\d+:/.test(value)) {
|
|
84
|
-
value = value.replace(/^\d+:\d+:/, "");
|
|
85
|
-
}
|
|
86
|
-
return value.replace(/:\d+:\d+$/, "").trim();
|
|
87
|
-
}
|
|
88
|
-
function parseMethodName(value) {
|
|
89
|
-
const match = /^(.+?)\s+([^\s(]+)\((.*)\)$/.exec(value);
|
|
90
|
-
if (!match) {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
return match[2]?.trim() || undefined;
|
|
94
|
-
}
|
|
95
|
-
function parseFieldName(value) {
|
|
96
|
-
const match = /^(.+?)\s+([^\s]+)$/.exec(value);
|
|
97
|
-
if (!match) {
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
return match[2]?.trim() || undefined;
|
|
101
|
-
}
|
|
102
|
-
function buildSymbolKey(record) {
|
|
103
|
-
return `${record.kind}|${record.owner ?? ""}|${record.name}|${record.descriptor ?? ""}`;
|
|
104
|
-
}
|
|
105
|
-
function classNameParts(classFqn) {
|
|
106
|
-
const separatorIndex = classFqn.lastIndexOf(".");
|
|
107
|
-
if (separatorIndex <= 0 || separatorIndex >= classFqn.length - 1) {
|
|
108
|
-
return {
|
|
109
|
-
owner: undefined,
|
|
110
|
-
name: classFqn
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
return {
|
|
114
|
-
owner: classFqn.slice(0, separatorIndex),
|
|
115
|
-
name: classFqn.slice(separatorIndex + 1)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function createClassSymbolRecord(className) {
|
|
119
|
-
const symbol = normalizeMappedSymbolOutput(className.trim());
|
|
120
|
-
const parts = classNameParts(symbol);
|
|
121
|
-
return {
|
|
122
|
-
kind: "class",
|
|
123
|
-
symbol,
|
|
124
|
-
owner: parts.owner,
|
|
125
|
-
name: parts.name
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function createFieldSymbolRecord(owner, fieldName) {
|
|
129
|
-
const normalizedOwner = normalizeMappedSymbolOutput(owner.trim());
|
|
130
|
-
const normalizedName = fieldName.trim();
|
|
131
|
-
return {
|
|
132
|
-
kind: "field",
|
|
133
|
-
symbol: `${normalizedOwner}.${normalizedName}`,
|
|
134
|
-
owner: normalizedOwner,
|
|
135
|
-
name: normalizedName
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
function createMethodSymbolRecord(owner, methodName, descriptor) {
|
|
139
|
-
const normalizedOwner = normalizeMappedSymbolOutput(owner.trim());
|
|
140
|
-
const normalizedName = methodName.trim();
|
|
141
|
-
const normalizedDescriptor = descriptor?.trim() || undefined;
|
|
142
|
-
return {
|
|
143
|
-
kind: "method",
|
|
144
|
-
symbol: `${normalizedOwner}.${normalizedName}${normalizedDescriptor ?? ""}`,
|
|
145
|
-
owner: normalizedOwner,
|
|
146
|
-
name: normalizedName,
|
|
147
|
-
descriptor: normalizedDescriptor
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function parseInputSymbol(symbol) {
|
|
151
|
-
const trimmed = symbol.trim();
|
|
152
|
-
if (!trimmed || /\s/.test(trimmed)) {
|
|
153
|
-
return undefined;
|
|
154
|
-
}
|
|
155
|
-
const openIndex = trimmed.indexOf("(");
|
|
156
|
-
if (openIndex >= 0) {
|
|
157
|
-
const closeIndex = trimmed.indexOf(")", openIndex);
|
|
158
|
-
if (closeIndex < 0) {
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
const ownerAndMethod = splitOwnerAndName(trimmed.slice(0, openIndex));
|
|
162
|
-
if (!ownerAndMethod) {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
const descriptor = trimmed.slice(openIndex);
|
|
166
|
-
return createMethodSymbolRecord(ownerAndMethod.owner, ownerAndMethod.name, descriptor);
|
|
167
|
-
}
|
|
168
|
-
const ownerAndName = splitOwnerAndName(trimmed);
|
|
169
|
-
if (!ownerAndName) {
|
|
170
|
-
return createClassSymbolRecord(trimmed);
|
|
171
|
-
}
|
|
172
|
-
if (/^[A-Z$]/.test(ownerAndName.name)) {
|
|
173
|
-
return createClassSymbolRecord(trimmed);
|
|
174
|
-
}
|
|
175
|
-
return createFieldSymbolRecord(ownerAndName.owner, ownerAndName.name);
|
|
176
|
-
}
|
|
177
|
-
function exactLookupKeys(record) {
|
|
178
|
-
const keys = new Set([record.symbol]);
|
|
179
|
-
if (record.kind === "method" && record.owner && record.descriptor) {
|
|
180
|
-
keys.add(`${record.owner}.${record.name}`);
|
|
181
|
-
}
|
|
182
|
-
return [...keys];
|
|
183
|
-
}
|
|
184
|
-
function simpleLookupKeys(record) {
|
|
185
|
-
if (record.kind === "class") {
|
|
186
|
-
return [record.name];
|
|
187
|
-
}
|
|
188
|
-
if (record.kind === "field") {
|
|
189
|
-
return [record.name];
|
|
190
|
-
}
|
|
191
|
-
if (record.descriptor) {
|
|
192
|
-
return [record.name, `${record.name}${record.descriptor}`];
|
|
193
|
-
}
|
|
194
|
-
return [record.name];
|
|
195
|
-
}
|
|
196
|
-
function registerRecord(index, record) {
|
|
197
|
-
const key = buildSymbolKey(record);
|
|
198
|
-
if (!index.records.has(key)) {
|
|
199
|
-
index.records.set(key, record);
|
|
200
|
-
}
|
|
201
|
-
return key;
|
|
202
|
-
}
|
|
203
|
-
function addLookupEntries(index, fromRecord, toRecord) {
|
|
204
|
-
if (!fromRecord.symbol || !toRecord.symbol) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const targetKey = registerRecord(index, toRecord);
|
|
208
|
-
for (const key of exactLookupKeys(fromRecord)) {
|
|
209
|
-
addToSetMap(index.exact, key, targetKey);
|
|
210
|
-
for (const variant of normalizedVariants(key)) {
|
|
211
|
-
if (variant !== key) {
|
|
212
|
-
addToSetMap(index.normalized, variant, targetKey);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
for (const key of simpleLookupKeys(fromRecord)) {
|
|
217
|
-
addToSetMap(index.simple, key, targetKey);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
function mergeDirectionIndexes(target, source) {
|
|
221
|
-
const mergeMap = (targetMap, sourceMap) => {
|
|
222
|
-
for (const [key, values] of sourceMap.entries()) {
|
|
223
|
-
const existing = targetMap.get(key) ?? new Set();
|
|
224
|
-
for (const value of values) {
|
|
225
|
-
existing.add(value);
|
|
226
|
-
}
|
|
227
|
-
targetMap.set(key, existing);
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
mergeMap(target.exact, source.exact);
|
|
231
|
-
mergeMap(target.normalized, source.normalized);
|
|
232
|
-
mergeMap(target.simple, source.simple);
|
|
233
|
-
for (const [key, value] of source.records.entries()) {
|
|
234
|
-
if (!target.records.has(key)) {
|
|
235
|
-
target.records.set(key, value);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
function pairKey(sourceMapping, targetMapping) {
|
|
240
|
-
return `${sourceMapping}->${targetMapping}`;
|
|
241
|
-
}
|
|
242
|
-
function parsePairKey(key) {
|
|
243
|
-
const separator = key.indexOf("->");
|
|
244
|
-
const source = separator >= 0 ? key.slice(0, separator) : key;
|
|
245
|
-
const target = separator >= 0 ? key.slice(separator + 2) : "";
|
|
246
|
-
return {
|
|
247
|
-
sourceMapping: source,
|
|
248
|
-
targetMapping: target
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
function buildAdjacency(pairs) {
|
|
252
|
-
const adjacency = new Map();
|
|
253
|
-
for (const key of pairs.keys()) {
|
|
254
|
-
const { sourceMapping, targetMapping } = parsePairKey(key);
|
|
255
|
-
let neighbors = adjacency.get(sourceMapping);
|
|
256
|
-
if (!neighbors) {
|
|
257
|
-
neighbors = new Set();
|
|
258
|
-
adjacency.set(sourceMapping, neighbors);
|
|
259
|
-
}
|
|
260
|
-
neighbors.add(targetMapping);
|
|
261
|
-
}
|
|
262
|
-
return new Map([...adjacency.entries()].map(([mapping, neighbors]) => [mapping, [...neighbors]]));
|
|
263
|
-
}
|
|
264
|
-
function buildTargetRecordIndex(pairs) {
|
|
265
|
-
const recordsByTarget = new Map();
|
|
266
|
-
for (const [key, pair] of pairs.entries()) {
|
|
267
|
-
const { targetMapping } = parsePairKey(key);
|
|
268
|
-
let bucket = recordsByTarget.get(targetMapping);
|
|
269
|
-
if (!bucket) {
|
|
270
|
-
bucket = new Map();
|
|
271
|
-
recordsByTarget.set(targetMapping, bucket);
|
|
272
|
-
}
|
|
273
|
-
for (const record of pair.index.records.values()) {
|
|
274
|
-
bucket.set(buildSymbolKey(record), record);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return new Map([...recordsByTarget.entries()].map(([mapping, records]) => [mapping, [...records.values()]]));
|
|
278
|
-
}
|
|
279
|
-
function ensurePairIndex(indexes, from, to) {
|
|
280
|
-
const key = pairKey(from, to);
|
|
281
|
-
const existing = indexes.get(key);
|
|
282
|
-
if (existing) {
|
|
283
|
-
return existing;
|
|
284
|
-
}
|
|
285
|
-
const created = createDirectionIndex();
|
|
286
|
-
indexes.set(key, created);
|
|
287
|
-
return created;
|
|
288
|
-
}
|
|
289
|
-
/** Map of proguard primitive type names to JVM type characters. */
|
|
290
|
-
const PROGUARD_PRIMITIVES = {
|
|
291
|
-
void: "V", boolean: "Z", byte: "B", char: "C",
|
|
292
|
-
short: "S", int: "I", long: "J", float: "F", double: "D"
|
|
293
|
-
};
|
|
294
|
-
/**
|
|
295
|
-
* Convert a single proguard type (e.g. "int", "net.minecraft.Foo", "int[][]")
|
|
296
|
-
* to JVM notation (e.g. "I", "Lnet/minecraft/Foo;", "[[I").
|
|
297
|
-
* `classLookup` maps mojang class names → obfuscated class names (for the obfuscated descriptor).
|
|
298
|
-
* Pass `undefined` to skip class name translation (for mojang descriptors).
|
|
299
|
-
*/
|
|
300
|
-
function proguardTypeToJvm(type, classLookup) {
|
|
301
|
-
let arrayDepth = 0;
|
|
302
|
-
let base = type;
|
|
303
|
-
while (base.endsWith("[]")) {
|
|
304
|
-
arrayDepth += 1;
|
|
305
|
-
base = base.slice(0, -2);
|
|
306
|
-
}
|
|
307
|
-
const prefix = "[".repeat(arrayDepth);
|
|
308
|
-
const primitive = PROGUARD_PRIMITIVES[base];
|
|
309
|
-
if (primitive) {
|
|
310
|
-
return `${prefix}${primitive}`;
|
|
311
|
-
}
|
|
312
|
-
const translated = classLookup ? (classLookup.get(base) ?? base) : base;
|
|
313
|
-
return `${prefix}L${translated.replace(/\./g, "/")};`;
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Parse a proguard method signature (after stripLineInfo) into a JVM descriptor.
|
|
317
|
-
* Input format: "returnType methodName(paramType1,paramType2,...)"
|
|
318
|
-
* Returns `{ name, descriptor }` or `undefined` if parsing fails.
|
|
319
|
-
*/
|
|
320
|
-
function parseProguardMethod(value, classLookup) {
|
|
321
|
-
const match = /^(.+?)\s+([^\s(]+)\((.*)\)$/.exec(value);
|
|
322
|
-
if (!match) {
|
|
323
|
-
return undefined;
|
|
324
|
-
}
|
|
325
|
-
const returnType = match[1].trim();
|
|
326
|
-
const name = match[2].trim();
|
|
327
|
-
const params = match[3].trim();
|
|
328
|
-
if (!name) {
|
|
329
|
-
return undefined;
|
|
330
|
-
}
|
|
331
|
-
const paramParts = params ? params.split(",").map((p) => p.trim()) : [];
|
|
332
|
-
const paramDescriptor = paramParts.map((p) => proguardTypeToJvm(p, classLookup)).join("");
|
|
333
|
-
const returnDescriptor = proguardTypeToJvm(returnType, classLookup);
|
|
334
|
-
return { name, descriptor: `(${paramDescriptor})${returnDescriptor}` };
|
|
335
|
-
}
|
|
336
|
-
function parseClientMappings(text) {
|
|
337
|
-
const obfuscatedToMojang = createDirectionIndex();
|
|
338
|
-
const mojangToObfuscated = createDirectionIndex();
|
|
339
|
-
// Two-pass parsing: first collect class name mappings, then parse members with descriptors.
|
|
340
|
-
const lines = text.split(/\r?\n/);
|
|
341
|
-
// Pass 1: collect class name mappings (mojang → obfuscated)
|
|
342
|
-
const mojangToObfuscatedClass = new Map();
|
|
343
|
-
let classCount = 0;
|
|
344
|
-
for (const rawLine of lines) {
|
|
345
|
-
const line = rawLine.trim();
|
|
346
|
-
if (!line || line.startsWith("#")) {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
const classMatch = /^(.+?)\s+->\s+(.+):$/.exec(line);
|
|
350
|
-
if (classMatch) {
|
|
351
|
-
const mojangClass = classMatch[1]?.trim() ?? "";
|
|
352
|
-
const obfuscatedClass = classMatch[2]?.trim() ?? "";
|
|
353
|
-
if (mojangClass && obfuscatedClass) {
|
|
354
|
-
mojangToObfuscatedClass.set(mojangClass, obfuscatedClass);
|
|
355
|
-
classCount += 1;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
if (classCount === 0) {
|
|
360
|
-
throw createError({
|
|
361
|
-
code: ERROR_CODES.MAPPING_UNAVAILABLE,
|
|
362
|
-
message: "No class mappings could be parsed from client mappings."
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
// Pass 2: build full index with descriptors
|
|
366
|
-
let currentClass;
|
|
367
|
-
for (const rawLine of lines) {
|
|
368
|
-
const line = rawLine.trim();
|
|
369
|
-
if (!line || line.startsWith("#")) {
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
const classMatch = /^(.+?)\s+->\s+(.+):$/.exec(line);
|
|
373
|
-
if (classMatch) {
|
|
374
|
-
const mojangClass = classMatch[1]?.trim() ?? "";
|
|
375
|
-
const obfuscatedClass = classMatch[2]?.trim() ?? "";
|
|
376
|
-
if (!mojangClass || !obfuscatedClass) {
|
|
377
|
-
currentClass = undefined;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
currentClass = {
|
|
381
|
-
obfuscated: obfuscatedClass,
|
|
382
|
-
mojang: mojangClass
|
|
383
|
-
};
|
|
384
|
-
addLookupEntries(obfuscatedToMojang, createClassSymbolRecord(obfuscatedClass), createClassSymbolRecord(mojangClass));
|
|
385
|
-
addLookupEntries(mojangToObfuscated, createClassSymbolRecord(mojangClass), createClassSymbolRecord(obfuscatedClass));
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
if (!currentClass) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const arrowIndex = line.indexOf(" -> ");
|
|
392
|
-
if (arrowIndex < 0) {
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
const leftRaw = line.slice(0, arrowIndex).trim();
|
|
396
|
-
const rightRaw = line.slice(arrowIndex + 4).trim();
|
|
397
|
-
if (!leftRaw || !rightRaw) {
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
const mojangMemberSignature = stripLineInfo(leftRaw);
|
|
401
|
-
// Try method parsing with JVM descriptor
|
|
402
|
-
const obfuscatedMethod = parseProguardMethod(mojangMemberSignature, mojangToObfuscatedClass);
|
|
403
|
-
if (obfuscatedMethod) {
|
|
404
|
-
const mojangMethod = parseProguardMethod(mojangMemberSignature, undefined);
|
|
405
|
-
const obfuscatedDescriptor = obfuscatedMethod.descriptor;
|
|
406
|
-
const mojangDescriptor = mojangMethod?.descriptor;
|
|
407
|
-
addLookupEntries(obfuscatedToMojang, createMethodSymbolRecord(currentClass.obfuscated, rightRaw, obfuscatedDescriptor), createMethodSymbolRecord(currentClass.mojang, obfuscatedMethod.name, mojangDescriptor));
|
|
408
|
-
addLookupEntries(mojangToObfuscated, createMethodSymbolRecord(currentClass.mojang, obfuscatedMethod.name, mojangDescriptor), createMethodSymbolRecord(currentClass.obfuscated, rightRaw, obfuscatedDescriptor));
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
const fieldName = parseFieldName(mojangMemberSignature);
|
|
412
|
-
if (!fieldName) {
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
addLookupEntries(obfuscatedToMojang, createFieldSymbolRecord(currentClass.obfuscated, rightRaw), createFieldSymbolRecord(currentClass.mojang, fieldName));
|
|
416
|
-
addLookupEntries(mojangToObfuscated, createFieldSymbolRecord(currentClass.mojang, fieldName), createFieldSymbolRecord(currentClass.obfuscated, rightRaw));
|
|
417
|
-
}
|
|
418
|
-
const result = new Map();
|
|
419
|
-
result.set(pairKey("obfuscated", "mojang"), obfuscatedToMojang);
|
|
420
|
-
result.set(pairKey("mojang", "obfuscated"), mojangToObfuscated);
|
|
421
|
-
return result;
|
|
422
|
-
}
|
|
423
|
-
function normalizeTinyNamespace(namespace) {
|
|
424
|
-
const normalized = namespace.trim().toLowerCase();
|
|
425
|
-
if (normalized === "obfuscated" || normalized === "official") {
|
|
426
|
-
return "obfuscated";
|
|
427
|
-
}
|
|
428
|
-
if (normalized === "mojang") {
|
|
429
|
-
return "mojang";
|
|
430
|
-
}
|
|
431
|
-
if (normalized === "intermediary") {
|
|
432
|
-
return "intermediary";
|
|
433
|
-
}
|
|
434
|
-
if (normalized === "named" || normalized === "yarn") {
|
|
435
|
-
return "yarn";
|
|
436
|
-
}
|
|
437
|
-
return undefined;
|
|
438
|
-
}
|
|
439
|
-
function addPairRecords(target, records) {
|
|
440
|
-
for (const [sourceMapping, sourceRecord] of records.entries()) {
|
|
441
|
-
for (const [targetMapping, targetRecord] of records.entries()) {
|
|
442
|
-
if (sourceMapping === targetMapping) {
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
addLookupEntries(ensurePairIndex(target, sourceMapping, targetMapping), sourceRecord, targetRecord);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
function parseTinyMappings(text) {
|
|
450
|
-
const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
|
|
451
|
-
if (lines.length === 0) {
|
|
452
|
-
return new Map();
|
|
453
|
-
}
|
|
454
|
-
const header = lines[0].split("\t");
|
|
455
|
-
if (header.length < 5 || header[0] !== "tiny" || header[1] !== "2") {
|
|
456
|
-
return new Map();
|
|
457
|
-
}
|
|
458
|
-
const namespaceColumns = header.slice(3).map((namespace, index) => ({
|
|
459
|
-
mapping: normalizeTinyNamespace(namespace),
|
|
460
|
-
columnIndex: index + 1
|
|
461
|
-
}));
|
|
462
|
-
const recognized = namespaceColumns.filter((entry) => entry.mapping != null);
|
|
463
|
-
if (recognized.length < 2) {
|
|
464
|
-
return new Map();
|
|
465
|
-
}
|
|
466
|
-
const result = new Map();
|
|
467
|
-
const currentClassNames = new Map();
|
|
468
|
-
for (const line of lines.slice(1)) {
|
|
469
|
-
const columns = line.split("\t");
|
|
470
|
-
if (columns[0] === "c") {
|
|
471
|
-
const classRecords = new Map();
|
|
472
|
-
for (const namespace of recognized) {
|
|
473
|
-
const value = columns[namespace.columnIndex]?.trim() ?? "";
|
|
474
|
-
if (!value) {
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
currentClassNames.set(namespace.mapping, value);
|
|
478
|
-
classRecords.set(namespace.mapping, createClassSymbolRecord(value));
|
|
479
|
-
}
|
|
480
|
-
addPairRecords(result, classRecords);
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
if (columns[0] === "" && columns[1] === "f") {
|
|
484
|
-
const fieldRecords = new Map();
|
|
485
|
-
for (const namespace of recognized) {
|
|
486
|
-
const owner = currentClassNames.get(namespace.mapping);
|
|
487
|
-
const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
|
|
488
|
-
if (!owner || !value) {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
fieldRecords.set(namespace.mapping, createFieldSymbolRecord(owner, value));
|
|
492
|
-
}
|
|
493
|
-
addPairRecords(result, fieldRecords);
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
if (columns[0] === "" && columns[1] === "m") {
|
|
497
|
-
const descriptor = columns[2]?.trim() || undefined;
|
|
498
|
-
const methodRecords = new Map();
|
|
499
|
-
for (const namespace of recognized) {
|
|
500
|
-
const owner = currentClassNames.get(namespace.mapping);
|
|
501
|
-
const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
|
|
502
|
-
if (!owner || !value) {
|
|
503
|
-
continue;
|
|
504
|
-
}
|
|
505
|
-
methodRecords.set(namespace.mapping, createMethodSymbolRecord(owner, value, descriptor));
|
|
506
|
-
}
|
|
507
|
-
addPairRecords(result, methodRecords);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return result;
|
|
511
|
-
}
|
|
512
|
-
function addCandidates(target, index, symbols, kind, confidence) {
|
|
513
|
-
if (!symbols || symbols.size === 0) {
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const rank = MATCH_RANK[kind];
|
|
517
|
-
for (const key of symbols) {
|
|
518
|
-
const record = index.records.get(key);
|
|
519
|
-
if (!record) {
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
const current = target.get(key);
|
|
523
|
-
if (!current || rank > current.rank || (rank === current.rank && confidence > current.confidence)) {
|
|
524
|
-
target.set(key, {
|
|
525
|
-
key,
|
|
526
|
-
record,
|
|
527
|
-
matchKind: kind,
|
|
528
|
-
confidence,
|
|
529
|
-
rank
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
function lookupCandidates(index, query) {
|
|
535
|
-
const trimmedQuery = query.symbol.trim();
|
|
536
|
-
const collected = new Map();
|
|
537
|
-
addCandidates(collected, index, index.exact.get(trimmedQuery), "exact", 1);
|
|
538
|
-
for (const variant of normalizedVariants(trimmedQuery)) {
|
|
539
|
-
addCandidates(collected, index, index.normalized.get(variant), "normalized", 0.9);
|
|
540
|
-
}
|
|
541
|
-
if (query.kind === "method" && query.owner && query.descriptor) {
|
|
542
|
-
const descriptorlessKey = `${query.owner}.${query.name}`;
|
|
543
|
-
addCandidates(collected, index, index.exact.get(descriptorlessKey), "normalized", DESCRIPTOR_FALLBACK_CONFIDENCE);
|
|
544
|
-
for (const variant of normalizedVariants(descriptorlessKey)) {
|
|
545
|
-
addCandidates(collected, index, index.normalized.get(variant), "normalized", DESCRIPTOR_FALLBACK_CONFIDENCE);
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
const simpleKeys = new Set();
|
|
549
|
-
const shortName = simpleName(trimmedQuery);
|
|
550
|
-
if (shortName) {
|
|
551
|
-
simpleKeys.add(shortName);
|
|
552
|
-
}
|
|
553
|
-
if (query.kind !== "class") {
|
|
554
|
-
simpleKeys.add(query.name);
|
|
555
|
-
}
|
|
556
|
-
if (query.kind === "method" && query.descriptor) {
|
|
557
|
-
simpleKeys.add(`${query.name}${query.descriptor}`);
|
|
558
|
-
}
|
|
559
|
-
for (const key of simpleKeys) {
|
|
560
|
-
addCandidates(collected, index, index.simple.get(key), "simple-name", 0.75);
|
|
561
|
-
}
|
|
562
|
-
return [...collected.values()]
|
|
563
|
-
.sort((left, right) => {
|
|
564
|
-
if (right.confidence !== left.confidence) {
|
|
565
|
-
return right.confidence - left.confidence;
|
|
566
|
-
}
|
|
567
|
-
if (right.rank !== left.rank) {
|
|
568
|
-
return right.rank - left.rank;
|
|
569
|
-
}
|
|
570
|
-
return left.record.symbol.localeCompare(right.record.symbol);
|
|
571
|
-
})
|
|
572
|
-
.slice(0, MAX_CANDIDATES)
|
|
573
|
-
.map(({ record, matchKind, confidence }) => ({
|
|
574
|
-
symbol: record.symbol,
|
|
575
|
-
matchKind,
|
|
576
|
-
confidence,
|
|
577
|
-
kind: record.kind,
|
|
578
|
-
owner: record.owner,
|
|
579
|
-
name: record.name,
|
|
580
|
-
descriptor: record.descriptor
|
|
581
|
-
}));
|
|
582
|
-
}
|
|
583
|
-
function mappingPriorityFromInput(configPriority, override) {
|
|
584
|
-
if (override === "loom-first" || override === "maven-first") {
|
|
585
|
-
return override;
|
|
586
|
-
}
|
|
587
|
-
return configPriority;
|
|
588
|
-
}
|
|
589
|
-
function mappingSourceOrder(priority) {
|
|
590
|
-
if (priority === "maven-first") {
|
|
591
|
-
return ["maven", "loom-cache"];
|
|
592
|
-
}
|
|
593
|
-
return ["loom-cache", "maven"];
|
|
594
|
-
}
|
|
595
|
-
function requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) {
|
|
596
|
-
return sourceMapping !== "intermediary" &&
|
|
597
|
-
sourceMapping !== "yarn" &&
|
|
598
|
-
targetMapping !== "intermediary" &&
|
|
599
|
-
targetMapping !== "yarn";
|
|
600
|
-
}
|
|
601
|
-
function namespacePath(graph, sourceMapping, targetMapping) {
|
|
602
|
-
if (sourceMapping === targetMapping) {
|
|
603
|
-
return [sourceMapping];
|
|
604
|
-
}
|
|
605
|
-
const key = pairKey(sourceMapping, targetMapping);
|
|
606
|
-
if (graph.pathCache.has(key)) {
|
|
607
|
-
return graph.pathCache.get(key);
|
|
608
|
-
}
|
|
609
|
-
const { adjacency } = graph;
|
|
610
|
-
const queue = [sourceMapping];
|
|
611
|
-
let queueIndex = 0;
|
|
612
|
-
const parent = new Map([[sourceMapping, undefined]]);
|
|
613
|
-
while (queueIndex < queue.length) {
|
|
614
|
-
const current = queue[queueIndex];
|
|
615
|
-
queueIndex += 1;
|
|
616
|
-
if (current === targetMapping) {
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
const neighbors = adjacency.get(current);
|
|
620
|
-
if (!neighbors) {
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
for (const neighbor of neighbors) {
|
|
624
|
-
if (parent.has(neighbor)) {
|
|
625
|
-
continue;
|
|
626
|
-
}
|
|
627
|
-
parent.set(neighbor, current);
|
|
628
|
-
queue.push(neighbor);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (!parent.has(targetMapping)) {
|
|
632
|
-
graph.pathCache.set(key, undefined);
|
|
633
|
-
return undefined;
|
|
634
|
-
}
|
|
635
|
-
const reversedPath = [];
|
|
636
|
-
let cursor = targetMapping;
|
|
637
|
-
while (cursor) {
|
|
638
|
-
reversedPath.push(cursor);
|
|
639
|
-
cursor = parent.get(cursor);
|
|
640
|
-
}
|
|
641
|
-
const path = reversedPath.reverse();
|
|
642
|
-
graph.pathCache.set(key, path);
|
|
643
|
-
return path;
|
|
644
|
-
}
|
|
645
|
-
function pathUsesSource(pairs, path, source) {
|
|
646
|
-
for (let index = 0; index < path.length - 1; index += 1) {
|
|
647
|
-
const hop = pairs.get(pairKey(path[index], path[index + 1]));
|
|
648
|
-
if (hop?.source === source) {
|
|
649
|
-
return true;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
function pathToTransformChain(path) {
|
|
655
|
-
if (path.length <= 1) {
|
|
656
|
-
return [];
|
|
657
|
-
}
|
|
658
|
-
const transform = [];
|
|
659
|
-
for (let index = 0; index < path.length - 1; index += 1) {
|
|
660
|
-
transform.push(`mapping:${path[index]}->${path[index + 1]}`);
|
|
661
|
-
}
|
|
662
|
-
return transform;
|
|
663
|
-
}
|
|
664
|
-
function toLookupCandidate(record) {
|
|
665
|
-
return {
|
|
666
|
-
symbol: record.symbol,
|
|
667
|
-
matchKind: "exact",
|
|
668
|
-
confidence: 1,
|
|
669
|
-
kind: record.kind,
|
|
670
|
-
owner: record.owner,
|
|
671
|
-
name: record.name,
|
|
672
|
-
descriptor: record.descriptor
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
function toSymbolReference(record) {
|
|
676
|
-
return {
|
|
677
|
-
kind: record.kind,
|
|
678
|
-
name: record.kind === "class" ? record.symbol : record.name,
|
|
679
|
-
owner: record.kind === "class" ? undefined : record.owner,
|
|
680
|
-
descriptor: record.kind === "method" ? record.descriptor : undefined,
|
|
681
|
-
symbol: record.symbol
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
function toResolutionCandidate(candidate) {
|
|
685
|
-
return {
|
|
686
|
-
kind: candidate.kind,
|
|
687
|
-
name: candidate.kind === "class" ? candidate.symbol : candidate.name,
|
|
688
|
-
owner: candidate.kind === "class" ? undefined : candidate.owner,
|
|
689
|
-
descriptor: candidate.kind === "method" ? candidate.descriptor : undefined,
|
|
690
|
-
symbol: candidate.symbol,
|
|
691
|
-
matchKind: candidate.matchKind,
|
|
692
|
-
confidence: candidate.confidence
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
function invalidInputError(message, details) {
|
|
696
|
-
return createError({
|
|
697
|
-
code: ERROR_CODES.INVALID_INPUT,
|
|
698
|
-
message,
|
|
699
|
-
details
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
|
-
function normalizeMemberName(name) {
|
|
703
|
-
const normalized = name.trim();
|
|
704
|
-
if (!normalized || /[\s./()]/.test(normalized)) {
|
|
705
|
-
throw invalidInputError("name must be a simple member name without separators when kind is field or method.", {
|
|
706
|
-
name
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
return normalized;
|
|
710
|
-
}
|
|
711
|
-
function normalizeMethodDescriptor(descriptor) {
|
|
712
|
-
const normalized = descriptor?.trim() ?? "";
|
|
713
|
-
if (!normalized || !normalized.startsWith("(") || !normalized.includes(")")) {
|
|
714
|
-
throw invalidInputError("descriptor must be a valid JVM descriptor when kind=method.", {
|
|
715
|
-
descriptor
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
return normalized;
|
|
719
|
-
}
|
|
720
|
-
function normalizeQuerySymbol(input, signatureMode, options) {
|
|
721
|
-
if (input.kind !== "class" && input.kind !== "field" && input.kind !== "method") {
|
|
722
|
-
throw invalidInputError('kind must be one of "class", "field", or "method".', {
|
|
723
|
-
kind: input.kind
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
const normalizedName = input.name?.trim() ?? "";
|
|
727
|
-
if (!normalizedName) {
|
|
728
|
-
throw invalidInputError("name must be a non-empty string.", {
|
|
729
|
-
name: input.name
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
if (input.kind === "class") {
|
|
733
|
-
const owner = input.owner?.trim();
|
|
734
|
-
if (owner) {
|
|
735
|
-
throw invalidInputError("owner is not allowed when kind=class. Use name as FQCN.", {
|
|
736
|
-
owner: input.owner,
|
|
737
|
-
nextAction: 'Provide class as name, e.g. "net.minecraft.server.Main".'
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
if (input.descriptor?.trim()) {
|
|
741
|
-
throw invalidInputError("descriptor is not allowed when kind=class.", {
|
|
742
|
-
descriptor: input.descriptor
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
const className = normalizeMappedSymbolOutput(normalizedName);
|
|
746
|
-
if (!className.includes(".") && !options?.allowShortClassName) {
|
|
747
|
-
throw invalidInputError("name must be a fully qualified class name when kind=class.", {
|
|
748
|
-
name: input.name
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
const record = createClassSymbolRecord(className);
|
|
752
|
-
return {
|
|
753
|
-
record,
|
|
754
|
-
querySymbol: toSymbolReference(record)
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
const owner = normalizeMappedSymbolOutput(input.owner?.trim() ?? "");
|
|
758
|
-
if (!owner) {
|
|
759
|
-
throw invalidInputError("owner is required when kind is field or method.", {
|
|
760
|
-
owner: input.owner,
|
|
761
|
-
kind: input.kind
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
if (input.kind === "field") {
|
|
765
|
-
if (input.descriptor?.trim()) {
|
|
766
|
-
throw invalidInputError("descriptor is not allowed when kind=field.", {
|
|
767
|
-
descriptor: input.descriptor
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
const record = createFieldSymbolRecord(owner, normalizeMemberName(normalizedName));
|
|
771
|
-
return {
|
|
772
|
-
record,
|
|
773
|
-
querySymbol: toSymbolReference(record)
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
const descriptor = signatureMode === "name-only"
|
|
777
|
-
? (input.descriptor?.trim() || "")
|
|
778
|
-
: normalizeMethodDescriptor(input.descriptor);
|
|
779
|
-
const record = createMethodSymbolRecord(owner, normalizeMemberName(normalizedName), descriptor);
|
|
780
|
-
return {
|
|
781
|
-
record,
|
|
782
|
-
querySymbol: toSymbolReference(record)
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
function normalizeOwnerHint(ownerHint) {
|
|
786
|
-
const normalized = ownerHint?.trim();
|
|
787
|
-
if (!normalized) {
|
|
788
|
-
return undefined;
|
|
789
|
-
}
|
|
790
|
-
return normalizeMappedSymbolOutput(normalized);
|
|
791
|
-
}
|
|
792
|
-
function normalizeDescriptorHint(descriptorHint) {
|
|
793
|
-
const normalized = descriptorHint?.trim();
|
|
794
|
-
return normalized || undefined;
|
|
795
|
-
}
|
|
796
|
-
function applyDisambiguationHints(candidates, disambiguation) {
|
|
797
|
-
if (!disambiguation || candidates.length <= 1) {
|
|
798
|
-
return candidates;
|
|
799
|
-
}
|
|
800
|
-
let filtered = [...candidates];
|
|
801
|
-
const ownerHint = normalizeOwnerHint(disambiguation.ownerHint);
|
|
802
|
-
if (ownerHint) {
|
|
803
|
-
const ownerMatched = filtered.filter((candidate) => {
|
|
804
|
-
if (candidate.owner) {
|
|
805
|
-
return normalizeMappedSymbolOutput(candidate.owner) === ownerHint;
|
|
806
|
-
}
|
|
807
|
-
const normalizedSymbol = normalizeMappedSymbolOutput(candidate.symbol);
|
|
808
|
-
return normalizedSymbol.startsWith(`${ownerHint}.`);
|
|
809
|
-
});
|
|
810
|
-
if (ownerMatched.length > 0) {
|
|
811
|
-
filtered = ownerMatched;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
const descriptorHint = normalizeDescriptorHint(disambiguation.descriptorHint);
|
|
815
|
-
if (descriptorHint) {
|
|
816
|
-
const descriptorMatched = filtered.filter((candidate) => candidate.descriptor != null && candidate.descriptor === descriptorHint);
|
|
817
|
-
if (descriptorMatched.length > 0) {
|
|
818
|
-
filtered = descriptorMatched;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
return filtered;
|
|
822
|
-
}
|
|
823
|
-
function projectLookupCandidateDescriptor(candidate, sourceDescriptor, targetDescriptor) {
|
|
824
|
-
// Tiny mappings preserve method descriptors verbatim, so single-hop tiny paths often
|
|
825
|
-
// return the source descriptor even though the final symbol is already in the target
|
|
826
|
-
// namespace. Multi-hop paths that already produced a target-side descriptor are left
|
|
827
|
-
// unchanged by design.
|
|
828
|
-
if (candidate.kind !== "method" ||
|
|
829
|
-
!candidate.descriptor ||
|
|
830
|
-
!targetDescriptor ||
|
|
831
|
-
candidate.descriptor !== sourceDescriptor) {
|
|
832
|
-
return candidate;
|
|
833
|
-
}
|
|
834
|
-
return {
|
|
835
|
-
...candidate,
|
|
836
|
-
descriptor: targetDescriptor
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
function effectiveLoomSearchProjectPath(projectPath) {
|
|
840
|
-
return normalizeOptionalProjectPath(projectPath) ?? normalizeOptionalProjectPath(process.cwd());
|
|
841
|
-
}
|
|
842
|
-
function collectTargetRecords(graph, targetMapping) {
|
|
843
|
-
return [...(graph.recordsByTarget.get(targetMapping) ?? [])];
|
|
844
|
-
}
|
|
845
|
-
function normalizeIncludedKinds(inputKinds) {
|
|
846
|
-
const normalized = new Set();
|
|
847
|
-
const kinds = inputKinds ?? ["class", "field", "method"];
|
|
848
|
-
for (const kind of kinds) {
|
|
849
|
-
if (kind === "class" || kind === "field" || kind === "method") {
|
|
850
|
-
normalized.add(kind);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
if (normalized.size === 0) {
|
|
854
|
-
normalized.add("class");
|
|
855
|
-
normalized.add("field");
|
|
856
|
-
normalized.add("method");
|
|
857
|
-
}
|
|
858
|
-
return normalized;
|
|
859
|
-
}
|
|
860
|
-
function inferAmbiguityReasons(candidates, usedMojangClientMappings) {
|
|
861
|
-
if (candidates.length <= 1) {
|
|
862
|
-
return [];
|
|
863
|
-
}
|
|
864
|
-
const reasons = [];
|
|
865
|
-
const owners = [...new Set(candidates.map((c) => c.owner).filter(Boolean))];
|
|
866
|
-
if (owners.length > 1) {
|
|
867
|
-
reasons.push(`Multiple owner classes matched: ${owners.join(", ")}`);
|
|
868
|
-
}
|
|
869
|
-
const matchKinds = [...new Set(candidates.map((c) => c.matchKind))];
|
|
870
|
-
if (matchKinds.length > 1) {
|
|
871
|
-
reasons.push(`Candidates matched at different precision levels: ${matchKinds.join(", ")}`);
|
|
872
|
-
}
|
|
873
|
-
if (usedMojangClientMappings) {
|
|
874
|
-
const hasDescriptor = candidates.some((c) => c.descriptor);
|
|
875
|
-
const missingDescriptor = candidates.some((c) => !c.descriptor);
|
|
876
|
-
if (hasDescriptor && missingDescriptor) {
|
|
877
|
-
reasons.push("Method descriptor was lost through mojang-client-mappings path, causing broader matching.");
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (owners.length <= 1) {
|
|
881
|
-
const descriptors = [...new Set(candidates.map((c) => c.descriptor).filter(Boolean))];
|
|
882
|
-
if (descriptors.length > 1) {
|
|
883
|
-
reasons.push(`Overloaded method: ${descriptors.length} variants`);
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
if (reasons.length === 0) {
|
|
887
|
-
reasons.push(`${candidates.length} candidates matched with similar confidence scores.`);
|
|
888
|
-
}
|
|
889
|
-
return reasons;
|
|
890
|
-
}
|
|
891
|
-
function clampCandidateLimit(limit) {
|
|
892
|
-
if (!Number.isFinite(limit) || limit == null) {
|
|
893
|
-
return MAX_CANDIDATES;
|
|
894
|
-
}
|
|
895
|
-
return Math.max(1, Math.min(MAX_CANDIDATES, Math.trunc(limit)));
|
|
896
|
-
}
|
|
897
|
-
function limitResolutionCandidates(candidates, requestedLimit) {
|
|
898
|
-
const candidateCount = candidates.length;
|
|
899
|
-
const limit = clampCandidateLimit(requestedLimit);
|
|
900
|
-
const limitedCandidates = candidateCount > limit ? candidates.slice(0, limit) : candidates;
|
|
901
|
-
return {
|
|
902
|
-
candidates: limitedCandidates,
|
|
903
|
-
candidateCount,
|
|
904
|
-
...(limitedCandidates.length < candidateCount ? { candidatesTruncated: true } : {})
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
function clampRowLimit(limit) {
|
|
908
|
-
if (!Number.isFinite(limit) || limit == null) {
|
|
909
|
-
return undefined;
|
|
910
|
-
}
|
|
911
|
-
return Math.max(1, Math.min(5000, Math.trunc(limit)));
|
|
912
|
-
}
|
|
22
|
+
/* parsers extracted to src/mapping/parsers/{symbol-records,normalize,proguard,tiny}.ts */
|
|
913
23
|
export class MappingService {
|
|
914
24
|
config;
|
|
915
25
|
versionService;
|
|
916
26
|
fetchFn;
|
|
917
27
|
graphCache = new Map();
|
|
918
28
|
buildLocks = new Map();
|
|
29
|
+
resolutionCache = new Map();
|
|
30
|
+
static RESOLUTION_CACHE_MAX = 512;
|
|
31
|
+
static RESOLUTION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
32
|
+
resolutionCacheHits = 0;
|
|
33
|
+
resolutionCacheMisses = 0;
|
|
34
|
+
get resolutionCacheStats() {
|
|
35
|
+
return {
|
|
36
|
+
hits: this.resolutionCacheHits,
|
|
37
|
+
misses: this.resolutionCacheMisses,
|
|
38
|
+
size: this.resolutionCache.size
|
|
39
|
+
};
|
|
40
|
+
}
|
|
919
41
|
constructor(config, versionService = new VersionService(config), fetchFn = globalThis.fetch) {
|
|
920
42
|
this.config = config;
|
|
921
43
|
this.versionService = versionService;
|
|
@@ -932,9 +54,25 @@ export class MappingService {
|
|
|
932
54
|
}
|
|
933
55
|
});
|
|
934
56
|
}
|
|
935
|
-
|
|
57
|
+
// Normalize the effective signatureMode exactly once so every downstream path — query
|
|
58
|
+
// symbol normalization, the strict-overload filter, the cache key, and warning text —
|
|
59
|
+
// sees the same value. The public tool schema defaults to "name-only", so an omitted
|
|
60
|
+
// signatureMode reaching the service (e.g. internal callers, MCP resource handlers, the
|
|
61
|
+
// resolution cache) must default to "name-only" too, otherwise the service contradicts
|
|
62
|
+
// the advertised default and silently reverts to the old descriptor-required path.
|
|
63
|
+
// Callers that genuinely need strict descriptor matching pass `signatureMode: "exact"`
|
|
64
|
+
// explicitly.
|
|
65
|
+
const effectiveSignatureMode = input.signatureMode ?? "name-only";
|
|
66
|
+
const { record: queryRecord, querySymbol } = normalizeQuerySymbol(input, effectiveSignatureMode, {
|
|
936
67
|
allowShortClassName: input.kind === "class" && input.sourceMapping === "obfuscated"
|
|
937
68
|
});
|
|
69
|
+
const cacheKey = this.buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode);
|
|
70
|
+
const cached = this.resolutionCache.get(cacheKey);
|
|
71
|
+
if (cached && Date.now() - cached.cachedAt < MappingService.RESOLUTION_CACHE_TTL_MS) {
|
|
72
|
+
this.resolutionCacheHits += 1;
|
|
73
|
+
return cached.result;
|
|
74
|
+
}
|
|
75
|
+
this.resolutionCacheMisses += 1;
|
|
938
76
|
const sourceMapping = input.sourceMapping;
|
|
939
77
|
const targetMapping = input.targetMapping;
|
|
940
78
|
if (!SUPPORTED_MAPPINGS.has(sourceMapping) || !SUPPORTED_MAPPINGS.has(targetMapping)) {
|
|
@@ -992,13 +130,51 @@ export class MappingService {
|
|
|
992
130
|
const descriptorProjection = queryRecord.kind === "method" && queryRecord.descriptor
|
|
993
131
|
? this.projectMethodDescriptorToTarget(graph, path, queryRecord.descriptor)
|
|
994
132
|
: undefined;
|
|
995
|
-
|
|
996
|
-
|
|
133
|
+
// Partial projections are still useful for comparison: projectMethodDescriptorToTarget
|
|
134
|
+
// leaves unmapped `L...;` references unchanged, so JDK / external classes pass through
|
|
135
|
+
// while Minecraft class references get rewritten to the target namespace. Using the
|
|
136
|
+
// projected descriptor even when `complete === false` produces descriptors shaped like
|
|
137
|
+
// the stored records (`(Lnet/minecraft/class_1799;Ljava/lang/String;)V`) and avoids
|
|
138
|
+
// false negatives for the very common mixed MC + JDK descriptor shape.
|
|
139
|
+
const projectedDescriptor = descriptorProjection?.hadClassReferences ? descriptorProjection.descriptor : undefined;
|
|
140
|
+
let rawCandidates = this
|
|
997
141
|
.mapCandidatesAlongPath(graph, path, queryRecord)
|
|
998
142
|
.map((candidate) => queryRecord.kind === "method" && queryRecord.descriptor
|
|
999
143
|
? projectLookupCandidateDescriptor(candidate, queryRecord.descriptor, projectedDescriptor)
|
|
1000
144
|
: candidate);
|
|
1001
145
|
const warnings = [];
|
|
146
|
+
// signatureMode="exact" on kind=method must not return descriptorless fallback candidates
|
|
147
|
+
// (lookupCandidates adds owner+name fallbacks by design for the loose path). Without this
|
|
148
|
+
// filter a caller who supplied `foo(I)V` could be told `foo(Z)V` is the exact mapping,
|
|
149
|
+
// which would be wrong for migration tooling. Mirror resolveMethodMappingExact's strict
|
|
150
|
+
// behavior: keep only candidates whose descriptor equals the (projected) requested
|
|
151
|
+
// descriptor. If nothing passes, the normal "candidates.length === 0 -> not_found" path
|
|
152
|
+
// takes over. Partial projection is accepted here for the same reason as above — we do
|
|
153
|
+
// not reject mixed MC + JDK descriptors as mapping_unavailable just because the JDK
|
|
154
|
+
// class reference was not in the mapping graph.
|
|
155
|
+
if (queryRecord.kind === "method" &&
|
|
156
|
+
queryRecord.descriptor &&
|
|
157
|
+
effectiveSignatureMode === "exact") {
|
|
158
|
+
// Tiny v2 stores a single descriptor per method entry (typically in the obfuscated
|
|
159
|
+
// namespace) and shares it across every column, while client mappings attach a mojang
|
|
160
|
+
// descriptor on the mojang side and an obfuscated descriptor on the obfuscated side.
|
|
161
|
+
// In multi-hop paths (e.g. mojang -> obfuscated -> intermediary -> yarn) the final
|
|
162
|
+
// candidate's owner and name live in the target namespace but its descriptor can still
|
|
163
|
+
// be the obfuscated form that rode along the Tiny hop. A strict filter that compared
|
|
164
|
+
// only against the fully projected target descriptor dropped those valid candidates
|
|
165
|
+
// and produced false `not_found` for common Mojang -> Yarn lookups. Accept any
|
|
166
|
+
// candidate whose descriptor matches the caller's descriptor, the target-space
|
|
167
|
+
// projection, or the obfuscated-space projection — the three forms that actually
|
|
168
|
+
// appear in the mapping graph.
|
|
169
|
+
const strictDescriptor = projectedDescriptor ?? queryRecord.descriptor;
|
|
170
|
+
const acceptedDescriptors = new Set([queryRecord.descriptor, strictDescriptor]);
|
|
171
|
+
const toObfuscatedPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
172
|
+
if (toObfuscatedPath) {
|
|
173
|
+
const obfuscatedProjection = this.projectMethodDescriptorToTarget(graph, toObfuscatedPath, queryRecord.descriptor);
|
|
174
|
+
acceptedDescriptors.add(obfuscatedProjection.descriptor);
|
|
175
|
+
}
|
|
176
|
+
rawCandidates = rawCandidates.filter((candidate) => candidate.descriptor !== undefined && acceptedDescriptors.has(candidate.descriptor));
|
|
177
|
+
}
|
|
1002
178
|
const disambiguatedCandidates = applyDisambiguationHints(rawCandidates, input.disambiguation);
|
|
1003
179
|
if (rawCandidates.length > disambiguatedCandidates.length) {
|
|
1004
180
|
warnings.push(`Disambiguation hints narrowed candidates from ${rawCandidates.length} to ${disambiguatedCandidates.length}.`);
|
|
@@ -1016,12 +192,21 @@ export class MappingService {
|
|
|
1016
192
|
}
|
|
1017
193
|
else if (candidates.length > 1) {
|
|
1018
194
|
warnings.push(`Ambiguous mapping: ${candidates.length} candidates matched. Provide a stricter symbol input or disambiguation hints.`);
|
|
195
|
+
if (queryRecord.kind === "method") {
|
|
196
|
+
// find-mapping defaults to signatureMode="name-only", which discards any supplied
|
|
197
|
+
// descriptor. Telling the caller to "add descriptor" would be ineffective unless they
|
|
198
|
+
// also switch mode, so we point to the exact alternatives instead.
|
|
199
|
+
warnings.push("Retry with signatureMode=\"exact\" plus a JVM descriptor, or pass disambiguation.descriptorHint, or raise maxCandidates up to 200 to inspect the full candidate list.");
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list, or use disambiguation.ownerHint to narrow the search.");
|
|
203
|
+
}
|
|
1019
204
|
}
|
|
1020
205
|
const status = candidates.length === 0 ? "not_found" : candidates.length === 1 ? "resolved" : "ambiguous";
|
|
1021
206
|
const ambiguityReasons = candidates.length > 1
|
|
1022
207
|
? inferAmbiguityReasons(candidates, pathUsesSource(graph.pairs, path, "mojang-client-mappings"))
|
|
1023
208
|
: undefined;
|
|
1024
|
-
|
|
209
|
+
const output = {
|
|
1025
210
|
querySymbol,
|
|
1026
211
|
mappingContext,
|
|
1027
212
|
resolved: status === "resolved",
|
|
@@ -1034,6 +219,9 @@ export class MappingService {
|
|
|
1034
219
|
provenance: this.provenanceForPath(graph, path),
|
|
1035
220
|
ambiguityReasons
|
|
1036
221
|
};
|
|
222
|
+
this.resolutionCache.set(cacheKey, { result: output, cachedAt: Date.now() });
|
|
223
|
+
this.trimResolutionCache();
|
|
224
|
+
return output;
|
|
1037
225
|
}
|
|
1038
226
|
async ensureMappingAvailable(input) {
|
|
1039
227
|
const version = input.version.trim();
|
|
@@ -1078,7 +266,7 @@ export class MappingService {
|
|
|
1078
266
|
targetMapping,
|
|
1079
267
|
sourcePriority: priority,
|
|
1080
268
|
nextAction: "Try mapping=obfuscated which is always available.",
|
|
1081
|
-
|
|
269
|
+
...buildSuggestedCall({ tool: "resolve-artifact", params: { mapping: "obfuscated" } })
|
|
1082
270
|
}
|
|
1083
271
|
});
|
|
1084
272
|
}
|
|
@@ -1193,6 +381,9 @@ export class MappingService {
|
|
|
1193
381
|
}
|
|
1194
382
|
if (strictCandidates.length > 1) {
|
|
1195
383
|
warnings.push("Exact method mapping is ambiguous for owner+method+descriptor.");
|
|
384
|
+
if (limitedCandidates.candidatesTruncated) {
|
|
385
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list, or narrow the lookup via find-mapping disambiguation hints.");
|
|
386
|
+
}
|
|
1196
387
|
return {
|
|
1197
388
|
querySymbol,
|
|
1198
389
|
mappingContext,
|
|
@@ -1495,6 +686,9 @@ export class MappingService {
|
|
|
1495
686
|
const buildOutput = (querySymbol, matched, status) => {
|
|
1496
687
|
const candidates = matched.map((record) => toResolutionCandidate(toLookupCandidate(record)));
|
|
1497
688
|
const limitedCandidates = limitResolutionCandidates(candidates, input.maxCandidates);
|
|
689
|
+
if (status === "ambiguous" && limitedCandidates.candidatesTruncated) {
|
|
690
|
+
warnings.push("Raise maxCandidates up to 200 to inspect the full candidate list.");
|
|
691
|
+
}
|
|
1498
692
|
return {
|
|
1499
693
|
querySymbol,
|
|
1500
694
|
mappingContext,
|
|
@@ -1537,11 +731,40 @@ export class MappingService {
|
|
|
1537
731
|
if (input.signatureMode === "name-only") {
|
|
1538
732
|
const status = methodCandidates.length === 1 ? "resolved" : methodCandidates.length > 1 ? "ambiguous" : "not_found";
|
|
1539
733
|
if (status === "ambiguous") {
|
|
1540
|
-
|
|
734
|
+
// name-only discards any supplied descriptor, so telling the caller to "provide
|
|
735
|
+
// descriptor" would not disambiguate — they need to switch to signatureMode="exact".
|
|
736
|
+
warnings.push(`Multiple method overloads matched name "${queryRecord.name}" in owner "${queryRecord.owner}". Retry with signatureMode="exact" plus a JVM descriptor to pick one overload.`);
|
|
1541
737
|
}
|
|
1542
738
|
return buildOutput(querySymbol, methodCandidates, status);
|
|
1543
739
|
}
|
|
1544
|
-
|
|
740
|
+
// Tiny parsing stores a single descriptor per method entry (typically in the obfuscated
|
|
741
|
+
// namespace) and copies it into every namespace at load time. That means a descriptor
|
|
742
|
+
// supplied in `sourceMapping` coordinates will not string-compare equal to the record
|
|
743
|
+
// for any method whose descriptor references remapped Minecraft classes. Project the
|
|
744
|
+
// caller's descriptor to obfuscated coordinates first so class references line up with
|
|
745
|
+
// the stored record descriptors. The projection is accepted even when
|
|
746
|
+
// `projection.complete` is false: `projectMethodDescriptorToTarget` leaves every
|
|
747
|
+
// unresolvable `L...;` reference unchanged (JDK/external classes like
|
|
748
|
+
// `Ljava/lang/String;` are never in the mapping graph and pass through by design), so a
|
|
749
|
+
// partial projection still aligns the Minecraft class refs with the stored descriptor
|
|
750
|
+
// form while leaving external class refs identical to the user input. Falling back to
|
|
751
|
+
// verbatim comparison on `complete === false` would send mixed descriptors like
|
|
752
|
+
// `(Lnet/minecraft/world/item/ItemStack;Ljava/lang/String;)V` down the raw-compare path
|
|
753
|
+
// and produce false negatives in the most common lookup shape. When no class references
|
|
754
|
+
// exist at all (primitives-only descriptors such as `(I)V`) the projector marks
|
|
755
|
+
// `hadClassReferences === false` and we simply reuse the original descriptor.
|
|
756
|
+
const queryDescriptor = queryRecord.descriptor;
|
|
757
|
+
let effectiveDescriptor = queryDescriptor;
|
|
758
|
+
if (sourceMapping !== "obfuscated") {
|
|
759
|
+
const projectionPath = namespacePath(graph, sourceMapping, "obfuscated");
|
|
760
|
+
if (projectionPath) {
|
|
761
|
+
const projection = this.projectMethodDescriptorToTarget(graph, projectionPath, queryDescriptor);
|
|
762
|
+
if (projection.hadClassReferences) {
|
|
763
|
+
effectiveDescriptor = projection.descriptor;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const descriptorMatched = methodCandidates.filter((record) => record.descriptor === effectiveDescriptor || record.descriptor === queryDescriptor);
|
|
1545
768
|
if (descriptorMatched.length === 1) {
|
|
1546
769
|
return buildOutput(querySymbol, descriptorMatched, "resolved");
|
|
1547
770
|
}
|
|
@@ -1865,225 +1088,21 @@ export class MappingService {
|
|
|
1865
1088
|
}
|
|
1866
1089
|
}
|
|
1867
1090
|
async loadMojangPairs(version) {
|
|
1868
|
-
|
|
1869
|
-
let metadata;
|
|
1870
|
-
try {
|
|
1871
|
-
metadata = await this.versionService.resolveVersionMappings(version);
|
|
1872
|
-
}
|
|
1873
|
-
catch (caughtError) {
|
|
1874
|
-
return {
|
|
1875
|
-
pairs: new Map(),
|
|
1876
|
-
warnings: [
|
|
1877
|
-
`Failed to resolve version metadata for "${version}": ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`
|
|
1878
|
-
],
|
|
1879
|
-
mappingArtifact: `version:${version}`
|
|
1880
|
-
};
|
|
1881
|
-
}
|
|
1882
|
-
const clientMappingsUrl = metadata.clientMappingsUrl ?? metadata.mappingsUrl;
|
|
1883
|
-
if (!clientMappingsUrl) {
|
|
1884
|
-
warnings.push(`Minecraft version "${version}" does not expose client mappings URL.`);
|
|
1885
|
-
return {
|
|
1886
|
-
pairs: new Map(),
|
|
1887
|
-
warnings,
|
|
1888
|
-
mappingArtifact: metadata.versionDetailUrl
|
|
1889
|
-
};
|
|
1890
|
-
}
|
|
1891
|
-
const mappingsPath = join(this.config.cacheDir, "mappings", version, "client_mappings.txt");
|
|
1892
|
-
if (!existsSync(mappingsPath)) {
|
|
1893
|
-
await mkdir(dirname(mappingsPath), { recursive: true });
|
|
1894
|
-
const downloaded = await downloadToCache(clientMappingsUrl, mappingsPath, {
|
|
1895
|
-
fetchFn: this.fetchFn,
|
|
1896
|
-
retries: this.config.fetchRetries,
|
|
1897
|
-
timeoutMs: this.config.fetchTimeoutMs
|
|
1898
|
-
});
|
|
1899
|
-
if (!downloaded.ok || !downloaded.path) {
|
|
1900
|
-
warnings.push(`Failed to download client mappings from "${clientMappingsUrl}" (status: ${downloaded.statusCode ?? "unknown"}).`);
|
|
1901
|
-
return {
|
|
1902
|
-
pairs: new Map(),
|
|
1903
|
-
warnings,
|
|
1904
|
-
mappingArtifact: clientMappingsUrl
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
try {
|
|
1909
|
-
const content = await readFile(mappingsPath, "utf8");
|
|
1910
|
-
return {
|
|
1911
|
-
pairs: parseClientMappings(content),
|
|
1912
|
-
warnings,
|
|
1913
|
-
mappingArtifact: clientMappingsUrl
|
|
1914
|
-
};
|
|
1915
|
-
}
|
|
1916
|
-
catch (caughtError) {
|
|
1917
|
-
warnings.push(`Failed to parse client mappings for "${version}": ${caughtError instanceof Error ? caughtError.message : String(caughtError)}`);
|
|
1918
|
-
return {
|
|
1919
|
-
pairs: new Map(),
|
|
1920
|
-
warnings,
|
|
1921
|
-
mappingArtifact: clientMappingsUrl
|
|
1922
|
-
};
|
|
1923
|
-
}
|
|
1091
|
+
return loadMojangPairs(this.loaderDeps(), version);
|
|
1924
1092
|
}
|
|
1925
1093
|
async loadTinyPairsFromLoom(version, projectPath) {
|
|
1926
|
-
|
|
1927
|
-
const merged = new Map();
|
|
1928
|
-
const discoveredPaths = new Set();
|
|
1929
|
-
for (const root of searchRoots) {
|
|
1930
|
-
let discovered = [];
|
|
1931
|
-
const versionRoot = join(root, version);
|
|
1932
|
-
try {
|
|
1933
|
-
discovered = existsSync(versionRoot)
|
|
1934
|
-
? await fastGlob.glob(["**/*.tiny", "**/*.tinyv2"], {
|
|
1935
|
-
cwd: versionRoot,
|
|
1936
|
-
absolute: true,
|
|
1937
|
-
onlyFiles: true
|
|
1938
|
-
})
|
|
1939
|
-
: await fastGlob.glob([`${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tiny`, `${version.replace(GLOB_SPECIAL_CHARS, "\\$&")}/**/*.tinyv2`], {
|
|
1940
|
-
cwd: root,
|
|
1941
|
-
absolute: true,
|
|
1942
|
-
onlyFiles: true
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
catch {
|
|
1946
|
-
continue;
|
|
1947
|
-
}
|
|
1948
|
-
const byVersion = discovered
|
|
1949
|
-
.filter((path) => path.replaceAll("\\", "/").includes(`/${version}/`))
|
|
1950
|
-
.sort((left, right) => left.localeCompare(right));
|
|
1951
|
-
if (byVersion.length === 0) {
|
|
1952
|
-
continue;
|
|
1953
|
-
}
|
|
1954
|
-
for (const path of byVersion) {
|
|
1955
|
-
discoveredPaths.add(path);
|
|
1956
|
-
try {
|
|
1957
|
-
const content = await readFile(path, "utf8");
|
|
1958
|
-
const parsed = parseTinyMappings(content);
|
|
1959
|
-
for (const [key, index] of parsed.entries()) {
|
|
1960
|
-
const existing = merged.get(key);
|
|
1961
|
-
if (!existing) {
|
|
1962
|
-
merged.set(key, index);
|
|
1963
|
-
}
|
|
1964
|
-
else {
|
|
1965
|
-
mergeDirectionIndexes(existing, index);
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
catch {
|
|
1970
|
-
// best effort: skip unreadable or invalid files
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
}
|
|
1974
|
-
const orderedPaths = [...discoveredPaths].sort((left, right) => left.localeCompare(right));
|
|
1975
|
-
if (orderedPaths.length > 0) {
|
|
1976
|
-
return {
|
|
1977
|
-
pairs: merged,
|
|
1978
|
-
warnings: [],
|
|
1979
|
-
mappingArtifact: orderedPaths[0]
|
|
1980
|
-
};
|
|
1981
|
-
}
|
|
1982
|
-
return {
|
|
1983
|
-
pairs: new Map(),
|
|
1984
|
-
warnings: [`No Loom tiny mapping files matched version "${version}".`],
|
|
1985
|
-
mappingArtifact: "loom-cache:none"
|
|
1986
|
-
};
|
|
1094
|
+
return loadTinyPairsFromLoom(version, projectPath);
|
|
1987
1095
|
}
|
|
1988
1096
|
async loadTinyPairsFromMaven(version) {
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
const intermediaryUrls = [];
|
|
1993
|
-
const yarnUrls = [];
|
|
1994
|
-
const repoBases = repos.map((repo) => repo.replace(/\/+$/, ""));
|
|
1995
|
-
const yarnCoordinatesByRepo = await Promise.all(repoBases.map(async (base) => ({
|
|
1996
|
-
base,
|
|
1997
|
-
yarnCoordinates: await this.fetchYarnCoordinates(base, version)
|
|
1998
|
-
})));
|
|
1999
|
-
for (const { base, yarnCoordinates } of yarnCoordinatesByRepo) {
|
|
2000
|
-
intermediaryUrls.push(`${base}/net/fabricmc/intermediary/${version}/intermediary-${version}-v2.jar`, `${base}/net/fabricmc/intermediary/${version}/intermediary-${version}.jar`);
|
|
2001
|
-
for (const coordinate of yarnCoordinates) {
|
|
2002
|
-
yarnUrls.push(`${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}-v2.jar`, `${base}/net/fabricmc/yarn/${coordinate}/yarn-${coordinate}.jar`);
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
const allUrls = [...intermediaryUrls, ...yarnUrls];
|
|
2006
|
-
const parsedResults = await Promise.allSettled(allUrls.map(async (url) => {
|
|
2007
|
-
const downloaded = await downloadToCache(url, defaultDownloadPath(this.config.cacheDir, url), {
|
|
2008
|
-
fetchFn: this.fetchFn,
|
|
2009
|
-
retries: this.config.fetchRetries,
|
|
2010
|
-
timeoutMs: this.config.fetchTimeoutMs
|
|
2011
|
-
});
|
|
2012
|
-
if (!downloaded.ok || !downloaded.path) {
|
|
2013
|
-
return undefined;
|
|
2014
|
-
}
|
|
2015
|
-
return this.parseTinyFromJar(downloaded.path);
|
|
2016
|
-
}));
|
|
2017
|
-
for (const result of parsedResults) {
|
|
2018
|
-
if (result.status !== "fulfilled" || !result.value) {
|
|
2019
|
-
continue;
|
|
2020
|
-
}
|
|
2021
|
-
for (const [key, index] of result.value.entries()) {
|
|
2022
|
-
const existing = merged.get(key);
|
|
2023
|
-
if (!existing) {
|
|
2024
|
-
merged.set(key, index);
|
|
2025
|
-
}
|
|
2026
|
-
else {
|
|
2027
|
-
mergeDirectionIndexes(existing, index);
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
if (merged.size === 0) {
|
|
2032
|
-
warnings.push(`No Maven tiny mappings could be loaded for "${version}".`);
|
|
2033
|
-
}
|
|
1097
|
+
return loadTinyPairsFromMaven(this.loaderDeps(), version);
|
|
1098
|
+
}
|
|
1099
|
+
loaderDeps() {
|
|
2034
1100
|
return {
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
1101
|
+
config: this.config,
|
|
1102
|
+
fetchFn: this.fetchFn,
|
|
1103
|
+
versionService: this.versionService
|
|
2038
1104
|
};
|
|
2039
1105
|
}
|
|
2040
|
-
async parseTinyFromJar(jarPath) {
|
|
2041
|
-
const tinyEntries = (await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"), { continueOnError: true })).sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
2042
|
-
const merged = new Map();
|
|
2043
|
-
for (const entry of tinyEntries) {
|
|
2044
|
-
try {
|
|
2045
|
-
const parsed = parseTinyMappings(entry.content);
|
|
2046
|
-
for (const [key, index] of parsed.entries()) {
|
|
2047
|
-
const existing = merged.get(key);
|
|
2048
|
-
if (!existing) {
|
|
2049
|
-
merged.set(key, index);
|
|
2050
|
-
}
|
|
2051
|
-
else {
|
|
2052
|
-
mergeDirectionIndexes(existing, index);
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
catch {
|
|
2057
|
-
// skip malformed tiny entries
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
return merged;
|
|
2061
|
-
}
|
|
2062
|
-
async fetchYarnCoordinates(repoBase, version) {
|
|
2063
|
-
const metadataUrl = `${repoBase}/net/fabricmc/yarn/maven-metadata.xml`;
|
|
2064
|
-
try {
|
|
2065
|
-
const response = await this.fetchFn(metadataUrl);
|
|
2066
|
-
if (!response.ok) {
|
|
2067
|
-
return [];
|
|
2068
|
-
}
|
|
2069
|
-
const xml = await response.text();
|
|
2070
|
-
const versions = [...xml.matchAll(/<version>([^<]+)<\/version>/g)]
|
|
2071
|
-
.map((match) => match[1]?.trim() ?? "")
|
|
2072
|
-
.filter((value) => value.startsWith(`${version}+build.`));
|
|
2073
|
-
const sorted = versions.sort((left, right) => {
|
|
2074
|
-
const leftBuild = Number.parseInt(left.split("+build.")[1] ?? "0", 10);
|
|
2075
|
-
const rightBuild = Number.parseInt(right.split("+build.")[1] ?? "0", 10);
|
|
2076
|
-
return rightBuild - leftBuild;
|
|
2077
|
-
});
|
|
2078
|
-
if (sorted.length > 0) {
|
|
2079
|
-
return sorted.slice(0, 3);
|
|
2080
|
-
}
|
|
2081
|
-
return [version];
|
|
2082
|
-
}
|
|
2083
|
-
catch {
|
|
2084
|
-
return [version];
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
1106
|
trimGraphCache() {
|
|
2088
1107
|
const maxEntries = Math.max(1, this.config.maxMappingGraphCache ?? 16);
|
|
2089
1108
|
while (this.graphCache.size > maxEntries) {
|
|
@@ -2094,6 +1113,11 @@ export class MappingService {
|
|
|
2094
1113
|
this.graphCache.delete(oldestKey);
|
|
2095
1114
|
}
|
|
2096
1115
|
}
|
|
1116
|
+
// Note: in-flight buildLocks may re-populate graphCache after release.
|
|
1117
|
+
// Resolution cache entries created by concurrent findMapping() calls may also
|
|
1118
|
+
// survive this invalidation. Both are bounded by TTL (5 min) and will expire
|
|
1119
|
+
// naturally. A full epoch-based invalidation would add complexity for a rare
|
|
1120
|
+
// user-initiated operation (manage-cache).
|
|
2097
1121
|
releaseGraphCacheEntry(version, sourcePriority) {
|
|
2098
1122
|
const normalizedVersion = version.trim();
|
|
2099
1123
|
if (!normalizedVersion) {
|
|
@@ -2106,6 +1130,42 @@ export class MappingService {
|
|
|
2106
1130
|
this.graphCache.delete(key);
|
|
2107
1131
|
}
|
|
2108
1132
|
}
|
|
1133
|
+
const resolutionPrefix = `${normalizedVersion}\0`;
|
|
1134
|
+
for (const key of this.resolutionCache.keys()) {
|
|
1135
|
+
if (key.startsWith(resolutionPrefix)) {
|
|
1136
|
+
this.resolutionCache.delete(key);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
buildResolutionCacheKey(version, input, querySymbol, effectiveSignatureMode) {
|
|
1141
|
+
return [
|
|
1142
|
+
version,
|
|
1143
|
+
input.kind,
|
|
1144
|
+
querySymbol.symbol,
|
|
1145
|
+
querySymbol.descriptor ?? "",
|
|
1146
|
+
input.sourceMapping,
|
|
1147
|
+
input.targetMapping,
|
|
1148
|
+
input.sourcePriority ?? "",
|
|
1149
|
+
effectiveLoomSearchProjectPath(input.projectPath) ?? "",
|
|
1150
|
+
effectiveSignatureMode,
|
|
1151
|
+
String(input.maxCandidates ?? ""),
|
|
1152
|
+
JSON.stringify(input.disambiguation ?? "")
|
|
1153
|
+
].join("\0");
|
|
1154
|
+
}
|
|
1155
|
+
trimResolutionCache() {
|
|
1156
|
+
if (this.resolutionCache.size <= MappingService.RESOLUTION_CACHE_MAX)
|
|
1157
|
+
return;
|
|
1158
|
+
const now = Date.now();
|
|
1159
|
+
for (const [key, entry] of this.resolutionCache) {
|
|
1160
|
+
if (now - entry.cachedAt > MappingService.RESOLUTION_CACHE_TTL_MS) {
|
|
1161
|
+
this.resolutionCache.delete(key);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
while (this.resolutionCache.size > MappingService.RESOLUTION_CACHE_MAX) {
|
|
1165
|
+
const firstKey = this.resolutionCache.keys().next().value;
|
|
1166
|
+
if (firstKey !== undefined)
|
|
1167
|
+
this.resolutionCache.delete(firstKey);
|
|
1168
|
+
}
|
|
2109
1169
|
}
|
|
2110
1170
|
}
|
|
2111
1171
|
// ---------------------------------------------------------------------------
|