@adhisang/minecraft-modding-mcp 6.2.0 → 7.0.0-rc.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 +71 -0
- package/README.md +21 -5
- package/dist/cache-policy.d.ts +71 -0
- package/dist/cache-policy.js +83 -0
- package/dist/cache-registry.js +6 -6
- package/dist/cli.js +74 -3
- package/dist/compat-stdio-transport.d.ts +1 -1
- package/dist/compat-stdio-transport.js +13 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +8 -2
- package/dist/decompiler/vineflower.d.ts +1 -0
- package/dist/decompiler/vineflower.js +8 -5
- package/dist/entry-tools/analyze-mod-service.d.ts +70 -136
- package/dist/entry-tools/analyze-symbol-service.d.ts +112 -150
- package/dist/entry-tools/compare-minecraft-service.d.ts +59 -145
- package/dist/entry-tools/entry-tool-schema.d.ts +38 -4
- package/dist/entry-tools/entry-tool-schema.js +4 -1
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +235 -799
- package/dist/entry-tools/inspect-minecraft/internal.js +65 -28
- package/dist/entry-tools/inspect-minecraft-service.d.ts +372 -1736
- package/dist/entry-tools/inspect-minecraft-service.js +1 -1
- package/dist/entry-tools/manage-cache-service.d.ts +81 -91
- package/dist/entry-tools/validate-project/cases/project-summary.d.ts +7 -7
- package/dist/entry-tools/validate-project-service.d.ts +164 -592
- package/dist/entry-tools/verify-mixin-target-service.d.ts +3 -19
- package/dist/era-classifier.d.ts +161 -0
- package/dist/era-classifier.js +292 -0
- package/dist/error-mapping.js +9 -2
- package/dist/index.d.ts +42 -4
- package/dist/index.js +637 -475
- package/dist/java-process.d.ts +2 -0
- package/dist/java-process.js +22 -2
- package/dist/json-rpc-framing.d.ts +77 -1
- package/dist/json-rpc-framing.js +249 -13
- package/dist/mapping/loaders/tiny-loom-selection.d.ts +88 -0
- package/dist/mapping/loaders/tiny-loom-selection.js +223 -0
- package/dist/mapping/loaders/tiny-loom.js +45 -33
- package/dist/mapping/loaders/tiny-maven.js +6 -11
- package/dist/mapping/parsers/tiny.d.ts +57 -0
- package/dist/mapping/parsers/tiny.js +99 -22
- package/dist/mapping-service.d.ts +19 -0
- package/dist/mapping-service.js +93 -9
- package/dist/mcp-helpers.d.ts +19 -2
- package/dist/mcp-helpers.js +48 -6
- package/dist/minecraft-explorer-service.d.ts +1 -1
- package/dist/mixin/types.d.ts +8 -0
- package/dist/mod-analyzer.js +7 -7
- package/dist/mod-decompile-service.js +1 -0
- package/dist/nbt/java-nbt-codec.js +12 -2
- package/dist/nbt/json-patch.js +14 -3
- package/dist/nbt/pipeline.js +40 -3
- package/dist/nbt/typed-json.js +26 -1
- package/dist/registration-adapter.d.ts +32 -0
- package/dist/registration-adapter.js +52 -0
- package/dist/request-context.d.ts +7 -0
- package/dist/request-context.js +9 -0
- package/dist/resources.d.ts +1 -1
- package/dist/resources.js +25 -19
- package/dist/server-identity.d.ts +27 -0
- package/dist/server-identity.js +26 -0
- package/dist/source/access-validate.js +53 -0
- package/dist/source/artifact-resolver.d.ts +69 -1
- package/dist/source/artifact-resolver.js +215 -14
- package/dist/source/class-source.d.ts +22 -0
- package/dist/source/class-source.js +162 -29
- package/dist/source/did-you-mean.d.ts +12 -1
- package/dist/source/did-you-mean.js +6 -2
- package/dist/source/file-access.js +150 -46
- package/dist/source/indexer.js +1 -0
- package/dist/source/nested-jars.d.ts +19 -0
- package/dist/source/nested-jars.js +90 -21
- package/dist/source/shared-utils.d.ts +21 -0
- package/dist/source/shared-utils.js +23 -0
- package/dist/source-service.d.ts +12 -0
- package/dist/source-service.js +3 -0
- package/dist/stdio-supervisor.d.ts +357 -2
- package/dist/stdio-supervisor.js +1031 -80
- package/dist/storage/db.d.ts +2 -1
- package/dist/storage/db.js +15 -8
- package/dist/synthetic-decorator.d.ts +24 -0
- package/dist/synthetic-decorator.js +48 -0
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.d.ts +17 -1
- package/dist/tool-guidance.js +417 -13
- package/dist/tool-schema-registry.d.ts +2 -0
- package/dist/tool-schema-registry.js +4 -0
- package/dist/tool-schemas.d.ts +2214 -3915
- package/dist/tool-schemas.js +34 -7
- package/dist/types.d.ts +35 -0
- package/dist/v1-parity-schemas.d.ts +7 -0
- package/dist/v1-parity-schemas.js +5584 -0
- package/dist/version-diff-service.d.ts +33 -0
- package/dist/version-diff-service.js +148 -3
- package/dist/version-service.js +36 -14
- package/dist/warning-details.js +18 -1
- package/docs/README-ja.md +9 -3
- package/docs/tool-reference.md +267 -22
- package/package.json +12 -9
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { open, stat } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import v8 from "node:v8";
|
|
6
|
+
import { pairKey } from "../parsers/normalize.js";
|
|
7
|
+
import { parseTinyHeader } from "../parsers/tiny.js";
|
|
8
|
+
/** Upper bound on the bytes read while sniffing a file's header line. */
|
|
9
|
+
const HEADER_PROBE_BYTES = 64 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Bytes of JS heap one index slot costs, measured on Minecraft 1.21.10 loom
|
|
12
|
+
* mappings (2,080 MB peak / 6,755,994 slots ~= 323 B; the Mojang proguard index
|
|
13
|
+
* lands at 330 B). Rounded up so the derived budget errs toward stopping early.
|
|
14
|
+
*/
|
|
15
|
+
export const HEAP_BYTES_PER_INDEX_ENTRY = 340;
|
|
16
|
+
/** Share of the heap still free at load time that a single tiny load may claim. */
|
|
17
|
+
const HEAP_BUDGET_FRACTION = 0.7;
|
|
18
|
+
const MIN_INDEX_ENTRY_BUDGET = 1_000_000;
|
|
19
|
+
const MAX_INDEX_ENTRY_BUDGET = 64_000_000;
|
|
20
|
+
/**
|
|
21
|
+
* How many index slots one Loom tiny load may accumulate before it stops and
|
|
22
|
+
* warns instead of exhausting the heap.
|
|
23
|
+
*
|
|
24
|
+
* The default is derived from the live V8 heap limit, so raising
|
|
25
|
+
* `--max-old-space-size` raises the budget automatically. `MCP_LOOM_TINY_MAX_INDEX_ENTRIES`
|
|
26
|
+
* overrides it outright.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTinyIndexEntryBudget(value = process.env.MCP_LOOM_TINY_MAX_INDEX_ENTRIES, heapStats = v8.getHeapStatistics()) {
|
|
29
|
+
const override = Number.parseInt(value ?? "", 10);
|
|
30
|
+
if (Number.isFinite(override) && override > 0) {
|
|
31
|
+
return override;
|
|
32
|
+
}
|
|
33
|
+
const free = Math.max(0, heapStats.heap_size_limit - heapStats.used_heap_size);
|
|
34
|
+
const derived = Math.floor((free * HEAP_BUDGET_FRACTION) / HEAP_BYTES_PER_INDEX_ENTRY);
|
|
35
|
+
return Math.min(MAX_INDEX_ENTRY_BUDGET, Math.max(MIN_INDEX_ENTRY_BUDGET, derived));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read just the first line of a tiny file and derive its namespace layout.
|
|
39
|
+
* Returns `undefined` for anything {@link parseTinyHeader} rejects, so the body
|
|
40
|
+
* of an unusable file is never read.
|
|
41
|
+
*/
|
|
42
|
+
export async function readTinyFileCandidate(path) {
|
|
43
|
+
let handle;
|
|
44
|
+
try {
|
|
45
|
+
const info = await stat(path);
|
|
46
|
+
if (!info.isFile() || info.size === 0) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
handle = await open(path, "r");
|
|
50
|
+
const buffer = Buffer.allocUnsafe(Math.min(HEADER_PROBE_BYTES, info.size));
|
|
51
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
52
|
+
const chunk = buffer.subarray(0, bytesRead).toString("utf8");
|
|
53
|
+
const newlineIndex = chunk.search(/\r?\n/);
|
|
54
|
+
// A file whose first line does not fit the probe cannot be a tiny header.
|
|
55
|
+
const headerLine = newlineIndex >= 0 ? chunk.slice(0, newlineIndex) : bytesRead < HEADER_PROBE_BYTES ? chunk : "";
|
|
56
|
+
const header = parseTinyHeader(headerLine);
|
|
57
|
+
if (!header) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
path,
|
|
62
|
+
bytes: info.size,
|
|
63
|
+
descriptorNamespace: header.descriptorNamespace,
|
|
64
|
+
namespaces: header.namespaces
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await handle?.close().catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function hashFile(path) {
|
|
75
|
+
try {
|
|
76
|
+
const hash = createHash("sha256");
|
|
77
|
+
for await (const chunk of createReadStream(path)) {
|
|
78
|
+
hash.update(chunk);
|
|
79
|
+
}
|
|
80
|
+
return hash.digest("hex");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Every ordered namespace pair a candidate's rows would populate. */
|
|
87
|
+
export function candidatePairKeys(candidate) {
|
|
88
|
+
const keys = [];
|
|
89
|
+
for (const from of candidate.namespaces) {
|
|
90
|
+
for (const to of candidate.namespaces) {
|
|
91
|
+
if (from !== to) {
|
|
92
|
+
keys.push(pairKey(from, to));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return keys;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Richest first: more namespaces beats fewer; then a file whose descriptors are
|
|
100
|
+
* written in obfuscated coordinates beats one that is not, because that is the
|
|
101
|
+
* form callers query with and the form every other tiny source this server reads
|
|
102
|
+
* (Fabric intermediary and yarn artifacts, the Maven loader) produces; then larger
|
|
103
|
+
* beats smaller; then path order so the result never depends on filesystem
|
|
104
|
+
* enumeration order.
|
|
105
|
+
*
|
|
106
|
+
* The descriptor-coordinate rule also decides which rendering becomes the one the
|
|
107
|
+
* descriptor-conflict filter measures the rest against, so it must come before size:
|
|
108
|
+
* Loom's `mappings-base.tiny` is written with `intermediary` first AND is the larger
|
|
109
|
+
* file, and letting it win would leave the whole index carrying intermediary-coordinate
|
|
110
|
+
* descriptors.
|
|
111
|
+
*/
|
|
112
|
+
export function compareCandidates(left, right) {
|
|
113
|
+
if (left.namespaces.length !== right.namespaces.length) {
|
|
114
|
+
return right.namespaces.length - left.namespaces.length;
|
|
115
|
+
}
|
|
116
|
+
const leftCanonical = left.descriptorNamespace === "obfuscated" ? 1 : 0;
|
|
117
|
+
const rightCanonical = right.descriptorNamespace === "obfuscated" ? 1 : 0;
|
|
118
|
+
if (leftCanonical !== rightCanonical) {
|
|
119
|
+
return rightCanonical - leftCanonical;
|
|
120
|
+
}
|
|
121
|
+
if (left.bytes !== right.bytes) {
|
|
122
|
+
return right.bytes - left.bytes;
|
|
123
|
+
}
|
|
124
|
+
return left.path.localeCompare(right.path);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Decide which of the discovered tiny files to merge.
|
|
128
|
+
*
|
|
129
|
+
* Loom keeps several renderings of one layered mapping set side by side in one
|
|
130
|
+
* output directory: the final `mappings.tiny`, a `-mojang` rendering with an extra
|
|
131
|
+
* column, `-migrated` renderings per loader build, and a `mappings-base.tiny`
|
|
132
|
+
* written with the namespace columns rotated. Two filters apply:
|
|
133
|
+
*
|
|
134
|
+
* 1. Byte-identical files are collapsed. Loom copies the same rendering into every
|
|
135
|
+
* layered variant directory, so re-reading them is pure repeated work.
|
|
136
|
+
* 2. A file is dropped as an alternative rendering when ALL of the following hold:
|
|
137
|
+
* it declares its descriptors in a different namespace than the rendering
|
|
138
|
+
* already chosen, it sits in a directory that already yielded a selected file,
|
|
139
|
+
* and every namespace pair it carries is already covered. Tiny v2 writes each
|
|
140
|
+
* descriptor once, in the file's first namespace; merging an `intermediary`-first
|
|
141
|
+
* rendering into an `official`-first index registers every method a second time
|
|
142
|
+
* under an incompatible descriptor, which turns unique lookups into ambiguous
|
|
143
|
+
* ones. All three conditions are required so that a genuinely different mapping
|
|
144
|
+
* file — a standalone yarn tiny, a cache in another Gradle root — is never
|
|
145
|
+
* mistaken for a rendering of data already loaded.
|
|
146
|
+
*/
|
|
147
|
+
export async function selectTinyFiles(paths) {
|
|
148
|
+
const candidates = [];
|
|
149
|
+
const unsupported = [];
|
|
150
|
+
for (const path of paths) {
|
|
151
|
+
const candidate = await readTinyFileCandidate(path);
|
|
152
|
+
if (candidate) {
|
|
153
|
+
candidates.push(candidate);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
unsupported.push(path);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
candidates.sort(compareCandidates);
|
|
160
|
+
// Only equal-sized files can be byte-identical, so hash nothing unless a size
|
|
161
|
+
// actually collides.
|
|
162
|
+
const sizeCounts = new Map();
|
|
163
|
+
for (const candidate of candidates) {
|
|
164
|
+
sizeCounts.set(candidate.bytes, (sizeCounts.get(candidate.bytes) ?? 0) + 1);
|
|
165
|
+
}
|
|
166
|
+
const hashes = new Map();
|
|
167
|
+
const directoriesByHash = new Map();
|
|
168
|
+
for (const candidate of candidates) {
|
|
169
|
+
if ((sizeCounts.get(candidate.bytes) ?? 0) < 2) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const digest = await hashFile(candidate.path);
|
|
173
|
+
if (!digest) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const hash = `${candidate.bytes}:${digest}`;
|
|
177
|
+
hashes.set(candidate.path, hash);
|
|
178
|
+
const directories = directoriesByHash.get(hash) ?? new Set();
|
|
179
|
+
directories.add(dirname(candidate.path));
|
|
180
|
+
directoriesByHash.set(hash, directories);
|
|
181
|
+
}
|
|
182
|
+
const selected = [];
|
|
183
|
+
const descriptorConflicts = [];
|
|
184
|
+
const duplicateOf = new Map();
|
|
185
|
+
const seenHashes = new Map();
|
|
186
|
+
const coveredPairs = new Set();
|
|
187
|
+
// Directories a selected rendering was read from, including the directories its
|
|
188
|
+
// byte-identical copies live in: any of them may hold sibling renderings.
|
|
189
|
+
const renderedDirectories = new Set();
|
|
190
|
+
let descriptorNamespace;
|
|
191
|
+
for (const candidate of candidates) {
|
|
192
|
+
const hash = hashes.get(candidate.path);
|
|
193
|
+
if (hash) {
|
|
194
|
+
const previous = seenHashes.get(hash);
|
|
195
|
+
if (previous) {
|
|
196
|
+
duplicateOf.set(candidate.path, previous);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
seenHashes.set(hash, candidate.path);
|
|
200
|
+
}
|
|
201
|
+
const pairs = candidatePairKeys(candidate);
|
|
202
|
+
if (descriptorNamespace != null &&
|
|
203
|
+
candidate.descriptorNamespace !== descriptorNamespace &&
|
|
204
|
+
renderedDirectories.has(dirname(candidate.path)) &&
|
|
205
|
+
pairs.every((key) => coveredPairs.has(key))) {
|
|
206
|
+
descriptorConflicts.push(candidate);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
descriptorNamespace ??= candidate.descriptorNamespace;
|
|
210
|
+
for (const key of pairs) {
|
|
211
|
+
coveredPairs.add(key);
|
|
212
|
+
}
|
|
213
|
+
renderedDirectories.add(dirname(candidate.path));
|
|
214
|
+
if (hash) {
|
|
215
|
+
for (const directory of directoriesByHash.get(hash) ?? []) {
|
|
216
|
+
renderedDirectories.add(directory);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
selected.push(candidate);
|
|
220
|
+
}
|
|
221
|
+
return { selected, duplicateOf, descriptorConflicts, unsupported };
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=tiny-loom-selection.js.map
|
|
@@ -4,15 +4,14 @@ import { join } from "node:path";
|
|
|
4
4
|
import fastGlob from "fast-glob";
|
|
5
5
|
import { buildVersionSourceSearchRoots } from "../../gradle-paths.js";
|
|
6
6
|
import { effectiveLoomSearchProjectPath } from "../lookup.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { parseTinyMappingsInto } from "../parsers/tiny.js";
|
|
8
|
+
import { resolveTinyIndexEntryBudget, selectTinyFiles } from "./tiny-loom-selection.js";
|
|
9
9
|
const GLOB_SPECIAL_CHARS = /[\\!*+?()[\]{}@|]/g;
|
|
10
10
|
export async function loadTinyPairsFromLoom(version, projectPath, gradleUserHome) {
|
|
11
11
|
const searchRoots = buildVersionSourceSearchRoots({
|
|
12
12
|
projectPath: effectiveLoomSearchProjectPath(projectPath),
|
|
13
13
|
gradleUserHome
|
|
14
14
|
});
|
|
15
|
-
const merged = new Map();
|
|
16
15
|
const discoveredPaths = new Set();
|
|
17
16
|
for (const root of searchRoots) {
|
|
18
17
|
let discovered = [];
|
|
@@ -33,44 +32,57 @@ export async function loadTinyPairsFromLoom(version, projectPath, gradleUserHome
|
|
|
33
32
|
catch {
|
|
34
33
|
continue;
|
|
35
34
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (byVersion.length === 0) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
for (const path of byVersion) {
|
|
43
|
-
discoveredPaths.add(path);
|
|
44
|
-
try {
|
|
45
|
-
const content = await readFile(path, "utf8");
|
|
46
|
-
const parsed = parseTinyMappings(content);
|
|
47
|
-
for (const [key, index] of parsed.entries()) {
|
|
48
|
-
const existing = merged.get(key);
|
|
49
|
-
if (!existing) {
|
|
50
|
-
merged.set(key, index);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
mergeDirectionIndexes(existing, index);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// best effort: skip unreadable or invalid files
|
|
35
|
+
for (const path of discovered) {
|
|
36
|
+
if (path.replaceAll("\\", "/").includes(`/${version}/`)) {
|
|
37
|
+
discoveredPaths.add(path);
|
|
59
38
|
}
|
|
60
39
|
}
|
|
61
40
|
}
|
|
62
41
|
const orderedPaths = [...discoveredPaths].sort((left, right) => left.localeCompare(right));
|
|
63
|
-
if (orderedPaths.length
|
|
42
|
+
if (orderedPaths.length === 0) {
|
|
64
43
|
return {
|
|
65
|
-
pairs:
|
|
66
|
-
warnings: [],
|
|
67
|
-
mappingArtifact:
|
|
44
|
+
pairs: new Map(),
|
|
45
|
+
warnings: [`No Loom tiny mapping files matched version "${version}".`],
|
|
46
|
+
mappingArtifact: "loom-cache:none"
|
|
68
47
|
};
|
|
69
48
|
}
|
|
49
|
+
// Read headers only, then drop byte-identical copies and descriptor-namespace
|
|
50
|
+
// conflicts before any file body is loaded. See tiny-loom-selection.ts.
|
|
51
|
+
const selection = await selectTinyFiles(orderedPaths);
|
|
52
|
+
const warnings = [];
|
|
53
|
+
const merged = new Map();
|
|
54
|
+
const maxIndexEntries = resolveTinyIndexEntryBudget();
|
|
55
|
+
const mergedPaths = [];
|
|
56
|
+
let truncatedAt;
|
|
57
|
+
for (const candidate of selection.selected) {
|
|
58
|
+
if (truncatedAt) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const content = await readFile(candidate.path, "utf8");
|
|
63
|
+
const result = parseTinyMappingsInto(merged, content, { maxIndexEntries });
|
|
64
|
+
if (result.parsed) {
|
|
65
|
+
mergedPaths.push(candidate.path);
|
|
66
|
+
}
|
|
67
|
+
if (result.truncated) {
|
|
68
|
+
truncatedAt = candidate.path;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// best effort: skip unreadable or invalid files
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (truncatedAt) {
|
|
76
|
+
const skipped = selection.selected.length - mergedPaths.length;
|
|
77
|
+
warnings.push(`Loom tiny mappings for "${version}" hit the ${maxIndexEntries}-entry index budget while reading "${truncatedAt}"; ` +
|
|
78
|
+
`${skipped} of ${selection.selected.length} selected file(s) were left unread and some symbols may be missing. ` +
|
|
79
|
+
`Raise MCP_LOOM_TINY_MAX_INDEX_ENTRIES or start the server with a larger --max-old-space-size.`);
|
|
80
|
+
}
|
|
70
81
|
return {
|
|
71
|
-
pairs:
|
|
72
|
-
warnings
|
|
73
|
-
|
|
82
|
+
pairs: merged,
|
|
83
|
+
warnings,
|
|
84
|
+
// The richest merged file, not merely the alphabetically first discovered one.
|
|
85
|
+
mappingArtifact: mergedPaths[0] ?? orderedPaths[0]
|
|
74
86
|
};
|
|
75
87
|
}
|
|
76
88
|
//# sourceMappingURL=tiny-loom.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defaultDownloadPath, downloadToCache } from "../../repo-downloader.js";
|
|
2
2
|
import { collectMatchedJarEntriesAsUtf8 } from "../../source-jar-reader.js";
|
|
3
3
|
import { mergeDirectionIndexes } from "../parsers/symbol-records.js";
|
|
4
|
-
import {
|
|
4
|
+
import { parseTinyMappingsInto } from "../parsers/tiny.js";
|
|
5
5
|
async function fetchYarnCoordinates(fetchFn, repoBase, version) {
|
|
6
6
|
const metadataUrl = `${repoBase}/net/fabricmc/yarn/maven-metadata.xml`;
|
|
7
7
|
try {
|
|
@@ -29,19 +29,14 @@ async function fetchYarnCoordinates(fetchFn, repoBase, version) {
|
|
|
29
29
|
}
|
|
30
30
|
async function parseTinyFromJar(jarPath) {
|
|
31
31
|
const tinyEntries = (await collectMatchedJarEntriesAsUtf8(jarPath, (entry) => entry.toLowerCase().endsWith(".tiny") || entry.toLowerCase().endsWith(".tinyv2"), { continueOnError: true })).sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
32
|
+
// Parsed straight into the shared accumulator: a parse-then-merge loop would
|
|
33
|
+
// hold each entry's full index alongside the accumulated one. `ensurePairIndex`
|
|
34
|
+
// + `addLookupEntries` union into what is already there, matching what
|
|
35
|
+
// `mergeDirectionIndexes` did.
|
|
32
36
|
const merged = new Map();
|
|
33
37
|
for (const entry of tinyEntries) {
|
|
34
38
|
try {
|
|
35
|
-
|
|
36
|
-
for (const [key, index] of parsed.entries()) {
|
|
37
|
-
const existing = merged.get(key);
|
|
38
|
-
if (!existing) {
|
|
39
|
-
merged.set(key, index);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
mergeDirectionIndexes(existing, index);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
39
|
+
parseTinyMappingsInto(merged, entry.content);
|
|
45
40
|
}
|
|
46
41
|
catch {
|
|
47
42
|
// skip malformed tiny entries
|
|
@@ -5,5 +5,62 @@
|
|
|
5
5
|
import type { SourceMapping } from "../../types.js";
|
|
6
6
|
import type { DirectionIndex, MappingSymbolRecord, PairKey } from "../internal-types.js";
|
|
7
7
|
export declare function normalizeTinyNamespace(namespace: string): SourceMapping | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Namespace layout of a tiny v2 file, derivable from the header line alone.
|
|
10
|
+
*
|
|
11
|
+
* `descriptorNamespace` identifies the FIRST namespace column, normalized when this
|
|
12
|
+
* server recognizes it (so `official` and `obfuscated` compare equal) and otherwise
|
|
13
|
+
* the raw lower-cased token. Tiny v2 stores every field/method descriptor in that
|
|
14
|
+
* namespace's coordinates and never repeats it per namespace, so two files whose
|
|
15
|
+
* first column differs describe the same members with mutually incompatible
|
|
16
|
+
* descriptors. Callers that merge several files into one index must compare this
|
|
17
|
+
* value — see `src/mapping/loaders/tiny-loom-selection.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export type TinyHeader = {
|
|
20
|
+
descriptorNamespace: SourceMapping | string;
|
|
21
|
+
/** Recognized namespaces paired with their data-column offset, in file order. */
|
|
22
|
+
columns: Array<{
|
|
23
|
+
mapping: SourceMapping;
|
|
24
|
+
columnIndex: number;
|
|
25
|
+
}>;
|
|
26
|
+
/** Distinct recognized namespaces, in file order. */
|
|
27
|
+
namespaces: SourceMapping[];
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Parse only the header line of a tiny v2 file. Returns `undefined` when the line
|
|
31
|
+
* is not a tiny v2 header or declares fewer than two namespaces this server knows,
|
|
32
|
+
* which is exactly when {@link parseTinyMappingsInto} would contribute nothing.
|
|
33
|
+
* Lets callers classify a file without reading its body.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseTinyHeader(headerLine: string): TinyHeader | undefined;
|
|
8
36
|
export declare function addPairRecords(target: Map<PairKey, DirectionIndex>, records: Map<SourceMapping, MappingSymbolRecord>): void;
|
|
37
|
+
/** Total number of index slots held by a pair map; the unit the parse budget counts. */
|
|
38
|
+
export declare function countIndexEntries(pairs: Map<PairKey, DirectionIndex>): number;
|
|
39
|
+
export type TinyParseOptions = {
|
|
40
|
+
/**
|
|
41
|
+
* Stop parsing once the target holds this many index slots (see
|
|
42
|
+
* {@link countIndexEntries}). Guards a single unbounded accumulator against
|
|
43
|
+
* caches large enough to exhaust the V8 heap. Omit for no bound.
|
|
44
|
+
*/
|
|
45
|
+
maxIndexEntries?: number;
|
|
46
|
+
};
|
|
47
|
+
export type TinyParseResult = {
|
|
48
|
+
/** False when the header was not usable, so nothing was added. */
|
|
49
|
+
parsed: boolean;
|
|
50
|
+
/** True when `maxIndexEntries` stopped the parse before the last line. */
|
|
51
|
+
truncated: boolean;
|
|
52
|
+
/** Index slots held by the target after this parse. */
|
|
53
|
+
indexEntries: number;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Parse `text` directly into `target`.
|
|
57
|
+
*
|
|
58
|
+
* Writing into the caller's accumulator rather than into a private map and
|
|
59
|
+
* merging afterwards is what keeps multi-file loads inside the heap: a
|
|
60
|
+
* parse-then-merge loop holds the whole per-file index AND the accumulated index
|
|
61
|
+
* at the same time, roughly doubling peak usage on the largest file. Merging is
|
|
62
|
+
* not lost — `ensurePairIndex` + `addLookupEntries` union into whatever is
|
|
63
|
+
* already present, which is precisely what `mergeDirectionIndexes` did.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseTinyMappingsInto(target: Map<PairKey, DirectionIndex>, text: string, options?: TinyParseOptions): TinyParseResult;
|
|
9
66
|
export declare function parseTinyMappings(text: string): Map<PairKey, DirectionIndex>;
|
|
@@ -20,6 +20,40 @@ export function normalizeTinyNamespace(namespace) {
|
|
|
20
20
|
}
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse only the header line of a tiny v2 file. Returns `undefined` when the line
|
|
25
|
+
* is not a tiny v2 header or declares fewer than two namespaces this server knows,
|
|
26
|
+
* which is exactly when {@link parseTinyMappingsInto} would contribute nothing.
|
|
27
|
+
* Lets callers classify a file without reading its body.
|
|
28
|
+
*/
|
|
29
|
+
export function parseTinyHeader(headerLine) {
|
|
30
|
+
const header = headerLine.split("\t");
|
|
31
|
+
if (header.length < 5 || header[0] !== "tiny" || header[1] !== "2") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const columns = header
|
|
35
|
+
.slice(3)
|
|
36
|
+
.map((namespace, index) => ({
|
|
37
|
+
mapping: normalizeTinyNamespace(namespace),
|
|
38
|
+
columnIndex: index + 1
|
|
39
|
+
}))
|
|
40
|
+
.filter((entry) => entry.mapping != null);
|
|
41
|
+
if (columns.length < 2) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const namespaces = [];
|
|
45
|
+
for (const column of columns) {
|
|
46
|
+
if (!namespaces.includes(column.mapping)) {
|
|
47
|
+
namespaces.push(column.mapping);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const rawDescriptorNamespace = header[3].trim().toLowerCase();
|
|
51
|
+
return {
|
|
52
|
+
descriptorNamespace: normalizeTinyNamespace(rawDescriptorNamespace) ?? rawDescriptorNamespace,
|
|
53
|
+
columns,
|
|
54
|
+
namespaces
|
|
55
|
+
};
|
|
56
|
+
}
|
|
23
57
|
export function addPairRecords(target, records) {
|
|
24
58
|
for (const [sourceMapping, sourceRecord] of records.entries()) {
|
|
25
59
|
for (const [targetMapping, targetRecord] of records.entries()) {
|
|
@@ -30,30 +64,68 @@ export function addPairRecords(target, records) {
|
|
|
30
64
|
}
|
|
31
65
|
}
|
|
32
66
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
67
|
+
/** Total number of index slots held by a pair map; the unit the parse budget counts. */
|
|
68
|
+
export function countIndexEntries(pairs) {
|
|
69
|
+
let total = 0;
|
|
70
|
+
for (const index of pairs.values()) {
|
|
71
|
+
total += index.exact.size + index.normalized.size + index.simple.size + index.records.size;
|
|
37
72
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
73
|
+
return total;
|
|
74
|
+
}
|
|
75
|
+
/** How often the running index size is re-measured while parsing. */
|
|
76
|
+
const BUDGET_CHECK_INTERVAL = 512;
|
|
77
|
+
/**
|
|
78
|
+
* Parse `text` directly into `target`.
|
|
79
|
+
*
|
|
80
|
+
* Writing into the caller's accumulator rather than into a private map and
|
|
81
|
+
* merging afterwards is what keeps multi-file loads inside the heap: a
|
|
82
|
+
* parse-then-merge loop holds the whole per-file index AND the accumulated index
|
|
83
|
+
* at the same time, roughly doubling peak usage on the largest file. Merging is
|
|
84
|
+
* not lost — `ensurePairIndex` + `addLookupEntries` union into whatever is
|
|
85
|
+
* already present, which is precisely what `mergeDirectionIndexes` did.
|
|
86
|
+
*/
|
|
87
|
+
export function parseTinyMappingsInto(target, text, options) {
|
|
88
|
+
const budget = options?.maxIndexEntries;
|
|
89
|
+
const lines = text.split(/\r?\n/);
|
|
90
|
+
let headerIndex = -1;
|
|
91
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
92
|
+
if (lines[index].trim().length > 0) {
|
|
93
|
+
headerIndex = index;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
41
96
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
if (headerIndex < 0) {
|
|
98
|
+
return { parsed: false, truncated: false, indexEntries: countIndexEntries(target) };
|
|
99
|
+
}
|
|
100
|
+
const header = parseTinyHeader(lines[headerIndex]);
|
|
101
|
+
if (!header) {
|
|
102
|
+
return { parsed: false, truncated: false, indexEntries: countIndexEntries(target) };
|
|
103
|
+
}
|
|
104
|
+
let entries = countIndexEntries(target);
|
|
105
|
+
if (budget != null && entries >= budget) {
|
|
106
|
+
return { parsed: true, truncated: true, indexEntries: entries };
|
|
49
107
|
}
|
|
50
|
-
const result = new Map();
|
|
51
108
|
const currentClassNames = new Map();
|
|
52
|
-
|
|
109
|
+
let sinceBudgetCheck = 0;
|
|
110
|
+
for (let lineIndex = headerIndex + 1; lineIndex < lines.length; lineIndex += 1) {
|
|
111
|
+
const line = lines[lineIndex];
|
|
112
|
+
if (line.trim().length === 0) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (budget != null) {
|
|
116
|
+
sinceBudgetCheck += 1;
|
|
117
|
+
if (sinceBudgetCheck >= BUDGET_CHECK_INTERVAL) {
|
|
118
|
+
sinceBudgetCheck = 0;
|
|
119
|
+
entries = countIndexEntries(target);
|
|
120
|
+
if (entries >= budget) {
|
|
121
|
+
return { parsed: true, truncated: true, indexEntries: entries };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
53
125
|
const columns = line.split("\t");
|
|
54
126
|
if (columns[0] === "c") {
|
|
55
127
|
const classRecords = new Map();
|
|
56
|
-
for (const namespace of
|
|
128
|
+
for (const namespace of header.columns) {
|
|
57
129
|
const value = columns[namespace.columnIndex]?.trim() ?? "";
|
|
58
130
|
if (!value) {
|
|
59
131
|
continue;
|
|
@@ -61,12 +133,12 @@ export function parseTinyMappings(text) {
|
|
|
61
133
|
currentClassNames.set(namespace.mapping, value);
|
|
62
134
|
classRecords.set(namespace.mapping, createClassSymbolRecord(value));
|
|
63
135
|
}
|
|
64
|
-
addPairRecords(
|
|
136
|
+
addPairRecords(target, classRecords);
|
|
65
137
|
continue;
|
|
66
138
|
}
|
|
67
139
|
if (columns[0] === "" && columns[1] === "f") {
|
|
68
140
|
const fieldRecords = new Map();
|
|
69
|
-
for (const namespace of
|
|
141
|
+
for (const namespace of header.columns) {
|
|
70
142
|
const owner = currentClassNames.get(namespace.mapping);
|
|
71
143
|
const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
|
|
72
144
|
if (!owner || !value) {
|
|
@@ -74,13 +146,13 @@ export function parseTinyMappings(text) {
|
|
|
74
146
|
}
|
|
75
147
|
fieldRecords.set(namespace.mapping, createFieldSymbolRecord(owner, value));
|
|
76
148
|
}
|
|
77
|
-
addPairRecords(
|
|
149
|
+
addPairRecords(target, fieldRecords);
|
|
78
150
|
continue;
|
|
79
151
|
}
|
|
80
152
|
if (columns[0] === "" && columns[1] === "m") {
|
|
81
153
|
const descriptor = columns[2]?.trim() || undefined;
|
|
82
154
|
const methodRecords = new Map();
|
|
83
|
-
for (const namespace of
|
|
155
|
+
for (const namespace of header.columns) {
|
|
84
156
|
const owner = currentClassNames.get(namespace.mapping);
|
|
85
157
|
const value = columns[namespace.columnIndex + 2]?.trim() ?? "";
|
|
86
158
|
if (!owner || !value) {
|
|
@@ -88,9 +160,14 @@ export function parseTinyMappings(text) {
|
|
|
88
160
|
}
|
|
89
161
|
methodRecords.set(namespace.mapping, createMethodSymbolRecord(owner, value, descriptor));
|
|
90
162
|
}
|
|
91
|
-
addPairRecords(
|
|
163
|
+
addPairRecords(target, methodRecords);
|
|
92
164
|
}
|
|
93
165
|
}
|
|
166
|
+
return { parsed: true, truncated: false, indexEntries: countIndexEntries(target) };
|
|
167
|
+
}
|
|
168
|
+
export function parseTinyMappings(text) {
|
|
169
|
+
const result = new Map();
|
|
170
|
+
parseTinyMappingsInto(result, text);
|
|
94
171
|
return result;
|
|
95
172
|
}
|
|
96
173
|
//# sourceMappingURL=tiny.js.map
|
|
@@ -37,6 +37,25 @@ export declare class MappingService {
|
|
|
37
37
|
checkSymbolExists(input: SymbolExistenceInput): Promise<SymbolExistenceOutput>;
|
|
38
38
|
private mapRecordBetweenMappings;
|
|
39
39
|
private mapCandidatesAlongPath;
|
|
40
|
+
/**
|
|
41
|
+
* The owner names that a query owner is allowed to appear under once candidates have
|
|
42
|
+
* been projected along `path`.
|
|
43
|
+
*
|
|
44
|
+
* The query owner arrives in the SOURCE namespace while `mapCandidatesAlongPath`
|
|
45
|
+
* hands back records whose `owner` is the declaring class in the TARGET namespace
|
|
46
|
+
* (both parsers build member records with `createMethodSymbolRecord(<owner in that
|
|
47
|
+
* namespace>, ...)`), so the two are only comparable after the owner itself has been
|
|
48
|
+
* carried along the same path. Projecting the owner as a class record through
|
|
49
|
+
* `mapCandidatesAlongPath` is that carriage, and it is the same mechanism
|
|
50
|
+
* `projectMethodDescriptorToTarget` already uses for class references inside a
|
|
51
|
+
* descriptor.
|
|
52
|
+
*
|
|
53
|
+
* `simple-name` class matches are excluded on purpose: they mean "some other class
|
|
54
|
+
* that happens to share this simple name", which is exactly the foreign-owner noise
|
|
55
|
+
* the strict filter exists to remove. The source-namespace name is kept as an
|
|
56
|
+
* accepted alias so an owner that the mapping does not rename still matches itself.
|
|
57
|
+
*/
|
|
58
|
+
private acceptedOwnersAlongPath;
|
|
40
59
|
private projectMethodDescriptorToTarget;
|
|
41
60
|
private provenanceForPath;
|
|
42
61
|
/**
|