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