@adhisang/minecraft-modding-mcp 6.1.1 → 6.2.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 +30 -0
- package/README.md +4 -2
- package/dist/cache-registry.d.ts +1 -1
- package/dist/cache-registry.js +3 -0
- package/dist/entry-tools/analyze-mod-service.d.ts +12 -6
- package/dist/entry-tools/analyze-mod-service.js +37 -3
- package/dist/entry-tools/analyze-symbol-service.d.ts +2 -0
- package/dist/entry-tools/analyze-symbol-service.js +37 -2
- package/dist/entry-tools/inspect-minecraft/internal.d.ts +4 -0
- package/dist/entry-tools/inspect-minecraft/internal.js +28 -0
- package/dist/entry-tools/manage-cache-service.d.ts +4 -4
- package/dist/error-mapping.d.ts +13 -0
- package/dist/error-mapping.js +35 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +2 -0
- package/dist/index.js +37 -17
- package/dist/mapping/internal-types.d.ts +7 -0
- package/dist/mapping/types.d.ts +18 -0
- package/dist/mapping-service.js +16 -3
- package/dist/minecraft-explorer-service.d.ts +4 -0
- package/dist/minecraft-explorer-service.js +156 -17
- package/dist/mod-analyzer.d.ts +7 -0
- package/dist/mod-analyzer.js +28 -7
- package/dist/source/artifact-resolver.d.ts +2 -0
- package/dist/source/artifact-resolver.js +24 -1
- package/dist/source/class-source/members-builder.d.ts +4 -0
- package/dist/source/class-source/members-builder.js +3 -1
- package/dist/source/class-source.d.ts +2 -1
- package/dist/source/class-source.js +154 -17
- package/dist/source/did-you-mean.d.ts +14 -0
- package/dist/source/did-you-mean.js +79 -0
- package/dist/source/file-access.js +159 -3
- package/dist/source/indexer.js +72 -2
- package/dist/source/lifecycle/runtime-check.js +9 -5
- package/dist/source/nested-jars.d.ts +59 -0
- package/dist/source/nested-jars.js +198 -0
- package/dist/source/workspace-target.js +5 -2
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +82 -0
- package/dist/source-service.d.ts +36 -0
- package/dist/source-service.js +49 -6
- package/dist/stage-emitter.js +24 -8
- package/dist/stdio-supervisor.d.ts +92 -9
- package/dist/stdio-supervisor.js +915 -103
- package/dist/tool-contract-manifest.js +1 -1
- package/dist/tool-guidance.js +3 -1
- package/dist/tool-schemas.d.ts +1337 -149
- package/dist/tool-schemas.js +38 -7
- package/dist/types.d.ts +23 -0
- package/dist/workspace-mapping-service.d.ts +1 -0
- package/dist/workspace-mapping-service.js +120 -8
- package/docs/tool-reference.md +19 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { makeStageEmitter } from "./stage-emitter.js";
|
|
4
|
-
import { ZodError
|
|
4
|
+
import { ZodError } from "zod";
|
|
5
5
|
import { CompatStdioServerTransport } from "./compat-stdio-transport.js";
|
|
6
6
|
import { objectResult } from "./mcp-helpers.js";
|
|
7
7
|
import { prepareToolInput } from "./tool-input.js";
|
|
@@ -34,7 +34,7 @@ import { buildEntryToolMeta } from "./entry-tools/response-contract.js";
|
|
|
34
34
|
import { registerToolSchema } from "./tool-schema-registry.js";
|
|
35
35
|
import { buildSuggestedCall } from "./build-suggested-call.js";
|
|
36
36
|
import { applyErrorMetaExtensions, mapErrorToProblem } from "./tool-guidance.js";
|
|
37
|
-
import {
|
|
37
|
+
import { analyzeModJarSchema, analyzeModJarShape, batchClassMembersSchema, batchClassMembersShape, batchClassSourceSchema, batchClassSourceShape, batchMappingsSchema, batchMappingsShape, batchSymbolExistsSchema, batchSymbolExistsShape, checkSymbolExistsSchema, checkSymbolExistsShape, compareVersionsSchema, compareVersionsShape, decompileModJarSchema, decompileModJarShape, diffClassSignaturesSchema, diffClassSignaturesShape, emptySchema, findMappingSchema, findMappingShape, getArtifactFileSchema, getArtifactFileShape, getClassApiMatrixSchema, getClassApiMatrixShape, getClassMembersSchema, getClassMembersShape, getClassSourceSchema, getClassSourceShape, getModClassSourceSchema, getModClassSourceShape, getRegistryDataSchema, getRegistryDataShape, indexArtifactSchema, indexArtifactShape, jsonToNbtSchema, jsonToNbtShape, listArtifactFilesSchema, listArtifactFilesShape, listVersionsSchema, listVersionsShape, nbtApplyJsonPatchSchema, nbtApplyJsonPatchShape, nbtToJsonSchema, nbtToJsonShape, remapModJarSchema, remapModJarShape, resolveArtifactSchema, resolveArtifactShape, resolveMethodMappingExactSchema, resolveMethodMappingExactShape, resolveWorkspaceSymbolSchema, resolveWorkspaceSymbolShape, searchClassSourceSchema, searchClassSourceShape, searchModSourceSchema, searchModSourceShape, traceSymbolLifecycleSchema, traceSymbolLifecycleShape, validateAccessTransformerSchema, validateAccessTransformerShape, validateAccessWidenerSchema, validateAccessWidenerShape, validateMixinSchema, validateMixinShape, verifyMixinTargetSchema, verifyMixinTargetShape, findClassShape, findClassSchema } from "./tool-schemas.js";
|
|
38
38
|
export { mapErrorToProblem, applyErrorMetaExtensions };
|
|
39
39
|
if (!process.env.NODE_ENV) {
|
|
40
40
|
process.env.NODE_ENV = "production";
|
|
@@ -121,9 +121,11 @@ const inspectMinecraftService = new InspectMinecraftService({
|
|
|
121
121
|
searchClassSource: (input) => sourceService.searchClassSource(input),
|
|
122
122
|
getArtifactFile: (input) => sourceService.getArtifactFile(input),
|
|
123
123
|
listArtifactFiles: (input) => sourceService.listArtifactFiles(input),
|
|
124
|
-
detectProjectMinecraftVersion: (projectPath) => workspaceMappingService.detectProjectMinecraftVersion(projectPath)
|
|
124
|
+
detectProjectMinecraftVersion: (projectPath) => workspaceMappingService.detectProjectMinecraftVersion(projectPath),
|
|
125
|
+
listWorkspaceContexts: () => sourceService.workspaceContextCache.list()
|
|
125
126
|
});
|
|
126
127
|
const analyzeSymbolService = new AnalyzeSymbolService({
|
|
128
|
+
detectProjectMinecraftVersion: (projectPath) => workspaceMappingService.detectProjectMinecraftVersion(projectPath),
|
|
127
129
|
checkSymbolExists: (input) => sourceService.checkSymbolExists(input),
|
|
128
130
|
findMapping: (input) => sourceService.findMapping(input),
|
|
129
131
|
resolveMethodMappingExact: (input) => sourceService.resolveMethodMappingExact(input),
|
|
@@ -141,7 +143,8 @@ const analyzeModService = new AnalyzeModService({
|
|
|
141
143
|
decompileModJar: (input) => sourceService.decompileModJar(input),
|
|
142
144
|
getModClassSource: (input) => sourceService.getModClassSource(input),
|
|
143
145
|
searchModSource: (input) => sourceService.searchModSource(input),
|
|
144
|
-
remapModJar: (input) => remapModJar(input, config)
|
|
146
|
+
remapModJar: (input) => remapModJar(input, config),
|
|
147
|
+
getModClassMembers: (input) => sourceService.getModClassMembers(input)
|
|
145
148
|
});
|
|
146
149
|
const validateProjectService = new ValidateProjectService({
|
|
147
150
|
validateMixin: (input, options) => sourceService.validateMixin(input, options),
|
|
@@ -475,7 +478,7 @@ server.tool("analyze-symbol", "Top-level workflow tool for symbol existence, map
|
|
|
475
478
|
registerToolSchema("analyze-symbol", analyzeSymbolSchema);
|
|
476
479
|
server.tool("compare-minecraft", "Top-level workflow tool for version comparisons, class diffs, registry diffs, and migration overviews.", compareMinecraftShape, { readOnlyHint: true }, async (args) => runTool("compare-minecraft", args, compareMinecraftSchema, async (input) => compareMinecraftService.execute(input)));
|
|
477
480
|
registerToolSchema("compare-minecraft", compareMinecraftSchema);
|
|
478
|
-
server.tool("analyze-mod", "Top-level workflow tool for mod metadata inspection, decompile/search flows, class source, and safe remap previews/applies.", analyzeModShape, { readOnlyHint: false }, async (args) => runTool("analyze-mod", args, analyzeModSchema, async (input) => analyzeModService.execute(input)));
|
|
481
|
+
server.tool("analyze-mod", "Top-level workflow tool for mod metadata inspection, decompile/search flows, class source, bytecode-only class member reads, and safe remap previews/applies.", analyzeModShape, { readOnlyHint: false }, async (args) => runTool("analyze-mod", args, analyzeModSchema, async (input) => analyzeModService.execute(input)));
|
|
479
482
|
registerToolSchema("analyze-mod", analyzeModSchema);
|
|
480
483
|
server.tool("validate-project", "Top-level workflow tool for project summary, direct mixin validation, and access widener/access transformer validation.", validateProjectShape, { readOnlyHint: true }, async (args, extra) => runTool("validate-project", args, validateProjectSchema, async (input) => validateProjectService.execute(input, {
|
|
481
484
|
stageEmitter: makeStageEmitter(extra)
|
|
@@ -524,15 +527,32 @@ expertTool("resolve-artifact", "Resolve a source artifact and return artifact me
|
|
|
524
527
|
(Array.isArray(input.include) && input.include.includes("samples"))
|
|
525
528
|
})));
|
|
526
529
|
registerToolSchema("resolve-artifact", resolveArtifactSchema);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
530
|
+
// Flat tools accept `target` as an additive alternative to `artifactId`
|
|
531
|
+
// (mutually exclusive, enforced by each schema). A target is resolved to its
|
|
532
|
+
// artifactId here before the underlying artifactId-based service call.
|
|
533
|
+
async function resolveFlatArtifactId(input) {
|
|
534
|
+
if (input.artifactId) {
|
|
535
|
+
return input.artifactId;
|
|
536
|
+
}
|
|
537
|
+
const target = input.target;
|
|
538
|
+
// {kind:"artifact"} reuses an already-resolved artifact without a resolve
|
|
539
|
+
// round-trip. Workspace-context kinds carry their own projectPath when the
|
|
540
|
+
// target shape provides one; kinds that require workspace context beyond
|
|
541
|
+
// that fail with the resolver's own projectPath guidance.
|
|
542
|
+
if (target?.kind === "artifact" && target.artifactId) {
|
|
543
|
+
return target.artifactId;
|
|
544
|
+
}
|
|
545
|
+
const resolved = await sourceService.resolveArtifact({
|
|
546
|
+
target: target,
|
|
547
|
+
...(target && "projectPath" in target && target.projectPath
|
|
548
|
+
? { projectPath: target.projectPath }
|
|
549
|
+
: {})
|
|
550
|
+
});
|
|
551
|
+
return resolved.artifactId;
|
|
552
|
+
}
|
|
533
553
|
expertTool("find-class", "Resolve a simple or qualified class name to fully-qualified class names within an artifact. Use this before get-class-source when you only have a simple name.", findClassShape, { readOnlyHint: true }, async (args) => runTool("find-class", args, findClassSchema, async (input) => sourceService.findClass({
|
|
534
554
|
className: input.className,
|
|
535
|
-
artifactId: input
|
|
555
|
+
artifactId: await resolveFlatArtifactId(input),
|
|
536
556
|
limit: input.limit
|
|
537
557
|
})));
|
|
538
558
|
registerToolSchema("find-class", findClassSchema);
|
|
@@ -596,7 +616,7 @@ expertTool("search-class-source", "Search indexed class source files for one art
|
|
|
596
616
|
}
|
|
597
617
|
: undefined;
|
|
598
618
|
return sourceService.searchClassSource({
|
|
599
|
-
artifactId: input
|
|
619
|
+
artifactId: await resolveFlatArtifactId(input),
|
|
600
620
|
query: input.query,
|
|
601
621
|
intent: input.intent,
|
|
602
622
|
match: input.match,
|
|
@@ -611,13 +631,13 @@ expertTool("search-class-source", "Search indexed class source files for one art
|
|
|
611
631
|
}));
|
|
612
632
|
registerToolSchema("search-class-source", searchClassSourceSchema);
|
|
613
633
|
expertTool("get-artifact-file", "Get full source file content by artifactId and file path.", getArtifactFileShape, { readOnlyHint: true }, async (args) => runTool("get-artifact-file", args, getArtifactFileSchema, async (input) => sourceService.getArtifactFile({
|
|
614
|
-
artifactId: input
|
|
634
|
+
artifactId: await resolveFlatArtifactId(input),
|
|
615
635
|
filePath: input.filePath,
|
|
616
636
|
maxBytes: input.maxBytes
|
|
617
637
|
})));
|
|
618
638
|
registerToolSchema("get-artifact-file", getArtifactFileSchema);
|
|
619
639
|
expertTool("list-artifact-files", "List source file paths in an artifact with optional prefix filter and cursor-based pagination.", listArtifactFilesShape, { readOnlyHint: true }, async (args) => runTool("list-artifact-files", args, listArtifactFilesSchema, async (input) => sourceService.listArtifactFiles({
|
|
620
|
-
artifactId: input
|
|
640
|
+
artifactId: await resolveFlatArtifactId(input),
|
|
621
641
|
prefix: input.prefix,
|
|
622
642
|
limit: input.limit,
|
|
623
643
|
cursor: input.cursor
|
|
@@ -727,8 +747,8 @@ server.tool("json-to-nbt", "Encode typed NBT JSON to Java Edition NBT binary pay
|
|
|
727
747
|
compression: input.compression
|
|
728
748
|
}, nbtLimits))));
|
|
729
749
|
registerToolSchema("json-to-nbt", jsonToNbtSchema);
|
|
730
|
-
expertTool("index-artifact", "Rebuild indexed files/symbols metadata for an
|
|
731
|
-
artifactId: input
|
|
750
|
+
expertTool("index-artifact", "Rebuild indexed files/symbols metadata for an artifact addressed by artifactId or target. A new target is resolved and ingested first; force rebuilds an existing index.", indexArtifactShape, { readOnlyHint: false, idempotentHint: true }, async (args) => runTool("index-artifact", args, indexArtifactSchema, async (input) => sourceService.indexArtifact({
|
|
751
|
+
artifactId: await resolveFlatArtifactId(input),
|
|
732
752
|
force: input.force
|
|
733
753
|
})));
|
|
734
754
|
registerToolSchema("index-artifact", indexArtifactSchema);
|
|
@@ -56,6 +56,13 @@ export type CandidateAccumulator = {
|
|
|
56
56
|
};
|
|
57
57
|
export type LoadedGraph = {
|
|
58
58
|
version: string;
|
|
59
|
+
/**
|
|
60
|
+
* True when the runtime already uses deobfuscated names, so the graph is
|
|
61
|
+
* deliberately empty. Surfaces on outputs as
|
|
62
|
+
* mappingContext.unobfuscatedRuntime instead of a per-response warning
|
|
63
|
+
* sentence.
|
|
64
|
+
*/
|
|
65
|
+
unobfuscatedRuntime?: boolean;
|
|
59
66
|
priority: import("../types.js").MappingSourcePriority;
|
|
60
67
|
mode: GraphLoadMode;
|
|
61
68
|
pairs: Map<PairKey, PairRecord>;
|
package/dist/mapping/types.d.ts
CHANGED
|
@@ -38,6 +38,19 @@ export type SymbolResolutionOutput = {
|
|
|
38
38
|
sourceMapping: SourceMapping;
|
|
39
39
|
targetMapping?: SourceMapping;
|
|
40
40
|
sourcePriorityApplied: MappingSourcePriority;
|
|
41
|
+
/**
|
|
42
|
+
* The runtime already uses deobfuscated names, so the mapping graph is
|
|
43
|
+
* deliberately empty. Replaces the former per-response warning
|
|
44
|
+
* "Version X is unobfuscated; mapping graph is empty because the runtime
|
|
45
|
+
* already uses deobfuscated names."
|
|
46
|
+
*/
|
|
47
|
+
unobfuscatedRuntime?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Symbol existence was validated against runtime bytecode. Replaces the
|
|
50
|
+
* former per-response warning "Version X is unobfuscated; validated
|
|
51
|
+
* symbol existence against runtime bytecode."
|
|
52
|
+
*/
|
|
53
|
+
runtimeValidated?: boolean;
|
|
41
54
|
};
|
|
42
55
|
resolved: boolean;
|
|
43
56
|
status: SymbolResolutionStatus;
|
|
@@ -122,6 +135,11 @@ export type ClassApiMatrixRow = {
|
|
|
122
135
|
completeness: boolean;
|
|
123
136
|
};
|
|
124
137
|
export type ClassApiMatrixOutput = {
|
|
138
|
+
/**
|
|
139
|
+
* The runtime already uses deobfuscated names, so non-mojang matrix
|
|
140
|
+
* columns are empty by design (the mapping graph is deliberately empty).
|
|
141
|
+
*/
|
|
142
|
+
unobfuscatedRuntime?: boolean;
|
|
125
143
|
version: string;
|
|
126
144
|
className: string;
|
|
127
145
|
classNameMapping: SourceMapping;
|
package/dist/mapping-service.js
CHANGED
|
@@ -126,6 +126,9 @@ export class MappingService {
|
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
128
|
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath, input.gradleUserHome);
|
|
129
|
+
if (graph.unobfuscatedRuntime) {
|
|
130
|
+
mappingContext.unobfuscatedRuntime = true;
|
|
131
|
+
}
|
|
129
132
|
const path = namespacePath(graph, sourceMapping, targetMapping);
|
|
130
133
|
if (!path) {
|
|
131
134
|
return {
|
|
@@ -352,6 +355,9 @@ export class MappingService {
|
|
|
352
355
|
};
|
|
353
356
|
}
|
|
354
357
|
const graph = await this.loadGraph(version, priority, requiresOnlyObfuscatedMojangGraph(sourceMapping, targetMapping) ? "obfuscated-mojang-only" : "full", input.projectPath, input.gradleUserHome);
|
|
358
|
+
if (graph.unobfuscatedRuntime) {
|
|
359
|
+
mappingContext.unobfuscatedRuntime = true;
|
|
360
|
+
}
|
|
355
361
|
const path = namespacePath(graph, sourceMapping, targetMapping);
|
|
356
362
|
if (!path) {
|
|
357
363
|
return {
|
|
@@ -469,6 +475,7 @@ export class MappingService {
|
|
|
469
475
|
const priority = mappingPriorityFromInput(this.config.mappingSourcePriority, input.sourcePriority);
|
|
470
476
|
const graph = await this.loadGraph(version, priority, "full", undefined, input.gradleUserHome);
|
|
471
477
|
const warnings = [...graph.warnings];
|
|
478
|
+
const unobfuscatedRuntime = graph.unobfuscatedRuntime === true;
|
|
472
479
|
const includeKinds = normalizeIncludedKinds(input.includeKinds);
|
|
473
480
|
const pathCache = new Map();
|
|
474
481
|
const resolvePath = (sourceMapping, targetMapping) => {
|
|
@@ -514,6 +521,7 @@ export class MappingService {
|
|
|
514
521
|
},
|
|
515
522
|
rows: [],
|
|
516
523
|
rowCount: 0,
|
|
524
|
+
...(unobfuscatedRuntime ? { unobfuscatedRuntime: true } : {}),
|
|
517
525
|
warnings
|
|
518
526
|
};
|
|
519
527
|
}
|
|
@@ -638,6 +646,7 @@ export class MappingService {
|
|
|
638
646
|
version,
|
|
639
647
|
className,
|
|
640
648
|
classNameMapping,
|
|
649
|
+
...(unobfuscatedRuntime ? { unobfuscatedRuntime: true } : {}),
|
|
641
650
|
classIdentity: {
|
|
642
651
|
obfuscated: classByMapping.obfuscated?.symbol,
|
|
643
652
|
mojang: classByMapping.mojang?.symbol,
|
|
@@ -727,6 +736,9 @@ export class MappingService {
|
|
|
727
736
|
};
|
|
728
737
|
})();
|
|
729
738
|
const graph = await this.loadGraph(version, priority, sourceMapping === "mojang" ? "obfuscated-mojang-only" : "full", undefined, input.gradleUserHome);
|
|
739
|
+
if (graph.unobfuscatedRuntime) {
|
|
740
|
+
mappingContext.unobfuscatedRuntime = true;
|
|
741
|
+
}
|
|
730
742
|
const warnings = [...graph.warnings];
|
|
731
743
|
const records = collectTargetRecords(graph, sourceMapping);
|
|
732
744
|
if (records.length === 0) {
|
|
@@ -1115,9 +1127,10 @@ export class MappingService {
|
|
|
1115
1127
|
recordsByTarget: new Map(),
|
|
1116
1128
|
exactRecordIndex: new Map(),
|
|
1117
1129
|
classProjectionCache: new Map(),
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1130
|
+
// No warning sentence: consumers surface this as the structured
|
|
1131
|
+
// mappingContext.unobfuscatedRuntime flag instead.
|
|
1132
|
+
unobfuscatedRuntime: true,
|
|
1133
|
+
warnings: []
|
|
1121
1134
|
};
|
|
1122
1135
|
}
|
|
1123
1136
|
const graph = {
|
|
@@ -15,6 +15,10 @@ export interface SignatureMember {
|
|
|
15
15
|
jvmDescriptor: string;
|
|
16
16
|
accessFlags: number;
|
|
17
17
|
isSynthetic: boolean;
|
|
18
|
+
/** Rendered default value of an annotation-type member (AnnotationDefault). */
|
|
19
|
+
annotationDefault?: string;
|
|
20
|
+
/** Rendered runtime-visible annotations on the member, e.g. "@java.lang.Deprecated". */
|
|
21
|
+
annotations?: string[];
|
|
18
22
|
}
|
|
19
23
|
export interface GetSignatureInput {
|
|
20
24
|
fqn: string;
|
|
@@ -3,6 +3,7 @@ import { loadConfig } from "./config.js";
|
|
|
3
3
|
import { artifactSignatureFromFile, normalizeJarPath } from "./path-resolver.js";
|
|
4
4
|
import { createJarEntryReader } from "./source-jar-reader.js";
|
|
5
5
|
import { matchesMemberPattern } from "./source/member-pattern.js";
|
|
6
|
+
import { isUnobfuscatedVersion } from "./version-service.js";
|
|
6
7
|
const CLASSFILE_MAGIC = 0xcafebabe;
|
|
7
8
|
const MAX_INHERITANCE_DEPTH = 64;
|
|
8
9
|
const ACC_PUBLIC = 0x0001;
|
|
@@ -173,6 +174,21 @@ function toInternalName(fqn) {
|
|
|
173
174
|
function extractVersionFromPath(inputPath) {
|
|
174
175
|
return inputPath.match(/(\d+\.\d+(?:\.\d+)?)/)?.[1];
|
|
175
176
|
}
|
|
177
|
+
// Super classes/interfaces from these packages are never inside a Minecraft
|
|
178
|
+
// jar, so failing to resolve them during inherited-member expansion is
|
|
179
|
+
// expected, not a signal. Suppression is allowlist-only and per-prefix
|
|
180
|
+
// verified: JDK packages by definition, com.mojang.serialization because the
|
|
181
|
+
// DataFixerUpper serialization library ships as its own dependency jar
|
|
182
|
+
// (checked against 26.x client-only/common and 1.21.10 client jars). A broad
|
|
183
|
+
// com.mojang.* entry would be wrong — com.mojang.blaze3d.* IS client-jar
|
|
184
|
+
// content and its resolution failures must stay visible.
|
|
185
|
+
const KNOWN_ABSENT_PLATFORM_PREFIXES = [
|
|
186
|
+
"java/",
|
|
187
|
+
"javax/",
|
|
188
|
+
"jdk/",
|
|
189
|
+
"sun/",
|
|
190
|
+
"com/mojang/serialization/"
|
|
191
|
+
];
|
|
176
192
|
class SignatureCacheStore {
|
|
177
193
|
maxEntries;
|
|
178
194
|
nodes = new Map();
|
|
@@ -305,16 +321,111 @@ function readOptionalClassName(cp, index) {
|
|
|
305
321
|
}
|
|
306
322
|
return readClassName(cp, index);
|
|
307
323
|
}
|
|
324
|
+
function descriptorToClassName(descriptor) {
|
|
325
|
+
if (descriptor.startsWith("L") && descriptor.endsWith(";")) {
|
|
326
|
+
return descriptor.slice(1, -1).replace(/\//g, ".");
|
|
327
|
+
}
|
|
328
|
+
return descriptor;
|
|
329
|
+
}
|
|
330
|
+
function readNumericConstant(cp, index) {
|
|
331
|
+
const entry = cp[index];
|
|
332
|
+
if (entry && (entry.tag === 3 || entry.tag === 4 || entry.tag === 5 || entry.tag === 6)) {
|
|
333
|
+
return String(entry.numericValue);
|
|
334
|
+
}
|
|
335
|
+
return "<unknown-constant>";
|
|
336
|
+
}
|
|
337
|
+
// JVMS 4.7.16.1 element_value, rendered as compact Java-ish text. Every
|
|
338
|
+
// branch reads exactly its encoded bytes so nested/array values stay aligned.
|
|
339
|
+
function readElementValue(reader, cp) {
|
|
340
|
+
const tag = String.fromCharCode(reader.readU1());
|
|
341
|
+
switch (tag) {
|
|
342
|
+
case "B":
|
|
343
|
+
case "I":
|
|
344
|
+
case "S":
|
|
345
|
+
case "D":
|
|
346
|
+
case "F":
|
|
347
|
+
case "J":
|
|
348
|
+
return readNumericConstant(cp, reader.readU2());
|
|
349
|
+
case "C": {
|
|
350
|
+
const raw = readNumericConstant(cp, reader.readU2());
|
|
351
|
+
const code = Number(raw);
|
|
352
|
+
return Number.isFinite(code) ? `'${String.fromCharCode(code)}'` : raw;
|
|
353
|
+
}
|
|
354
|
+
case "Z": {
|
|
355
|
+
const raw = readNumericConstant(cp, reader.readU2());
|
|
356
|
+
return raw === "1" ? "true" : raw === "0" ? "false" : raw;
|
|
357
|
+
}
|
|
358
|
+
case "s":
|
|
359
|
+
return JSON.stringify(readUtf8(cp, reader.readU2()));
|
|
360
|
+
case "e": {
|
|
361
|
+
const typeDescriptor = readUtf8(cp, reader.readU2());
|
|
362
|
+
const constantName = readUtf8(cp, reader.readU2());
|
|
363
|
+
return `${descriptorToClassName(typeDescriptor)}.${constantName}`;
|
|
364
|
+
}
|
|
365
|
+
case "c":
|
|
366
|
+
return `${descriptorToClassName(readUtf8(cp, reader.readU2()))}.class`;
|
|
367
|
+
case "@":
|
|
368
|
+
return readAnnotationText(reader, cp);
|
|
369
|
+
case "[": {
|
|
370
|
+
const count = reader.readU2();
|
|
371
|
+
const parts = [];
|
|
372
|
+
for (let index = 0; index < count; index += 1) {
|
|
373
|
+
parts.push(readElementValue(reader, cp));
|
|
374
|
+
}
|
|
375
|
+
return `{${parts.join(", ")}}`;
|
|
376
|
+
}
|
|
377
|
+
default:
|
|
378
|
+
return "<unsupported-element-value>";
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function readAnnotationText(reader, cp) {
|
|
382
|
+
const typeDescriptor = readUtf8(cp, reader.readU2());
|
|
383
|
+
const pairCount = reader.readU2();
|
|
384
|
+
const pairs = [];
|
|
385
|
+
for (let index = 0; index < pairCount; index += 1) {
|
|
386
|
+
const name = readUtf8(cp, reader.readU2());
|
|
387
|
+
pairs.push(`${name} = ${readElementValue(reader, cp)}`);
|
|
388
|
+
}
|
|
389
|
+
return `@${descriptorToClassName(typeDescriptor)}${pairs.length > 0 ? `(${pairs.join(", ")})` : ""}`;
|
|
390
|
+
}
|
|
308
391
|
function readAttributes(reader, cp, count) {
|
|
309
392
|
const names = [];
|
|
393
|
+
let annotationDefault;
|
|
394
|
+
let annotations;
|
|
310
395
|
for (let index = 0; index < count; index += 1) {
|
|
311
396
|
const nameIndex = reader.readU2();
|
|
312
397
|
const length = reader.readU4();
|
|
313
398
|
const attributeName = readUtf8(cp, nameIndex);
|
|
314
399
|
names.push(attributeName);
|
|
315
|
-
reader.
|
|
400
|
+
const body = reader.readBytes(length);
|
|
401
|
+
// Malformed attribute bodies must never fail the whole class parse; the
|
|
402
|
+
// rendered value is best-effort metadata, not a structural field.
|
|
403
|
+
if (attributeName === "AnnotationDefault") {
|
|
404
|
+
try {
|
|
405
|
+
annotationDefault = readElementValue(new ByteReader(body), cp);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
annotationDefault = undefined;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else if (attributeName === "RuntimeVisibleAnnotations") {
|
|
412
|
+
try {
|
|
413
|
+
const bodyReader = new ByteReader(body);
|
|
414
|
+
const annotationCount = bodyReader.readU2();
|
|
415
|
+
const rendered = [];
|
|
416
|
+
for (let a = 0; a < annotationCount; a += 1) {
|
|
417
|
+
rendered.push(readAnnotationText(bodyReader, cp));
|
|
418
|
+
}
|
|
419
|
+
if (rendered.length > 0) {
|
|
420
|
+
annotations = rendered;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
annotations = undefined;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
316
427
|
}
|
|
317
|
-
return names;
|
|
428
|
+
return { names, annotationDefault, annotations };
|
|
318
429
|
}
|
|
319
430
|
function parseClassFile(buffer) {
|
|
320
431
|
const reader = new ByteReader(buffer);
|
|
@@ -336,17 +447,28 @@ function parseClassFile(buffer) {
|
|
|
336
447
|
cp[index] = { tag: 1, value: reader.readBytes(length).toString("utf8") };
|
|
337
448
|
break;
|
|
338
449
|
}
|
|
339
|
-
case 3:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
450
|
+
case 3: {
|
|
451
|
+
const bytes = reader.readBytes(4);
|
|
452
|
+
cp[index] = { tag, numericValue: bytes.readInt32BE(0) };
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
case 4: {
|
|
456
|
+
const bytes = reader.readBytes(4);
|
|
457
|
+
cp[index] = { tag, numericValue: bytes.readFloatBE(0) };
|
|
343
458
|
break;
|
|
344
|
-
|
|
345
|
-
case
|
|
346
|
-
reader.
|
|
347
|
-
cp[index] = { tag };
|
|
459
|
+
}
|
|
460
|
+
case 5: {
|
|
461
|
+
const bytes = reader.readBytes(8);
|
|
462
|
+
cp[index] = { tag, numericValue: bytes.readBigInt64BE(0).toString() };
|
|
348
463
|
index += 1;
|
|
349
464
|
break;
|
|
465
|
+
}
|
|
466
|
+
case 6: {
|
|
467
|
+
const bytes = reader.readBytes(8);
|
|
468
|
+
cp[index] = { tag, numericValue: bytes.readDoubleBE(0) };
|
|
469
|
+
index += 1;
|
|
470
|
+
break;
|
|
471
|
+
}
|
|
350
472
|
case 7:
|
|
351
473
|
case 8:
|
|
352
474
|
case 16:
|
|
@@ -389,13 +511,17 @@ function parseClassFile(buffer) {
|
|
|
389
511
|
const nameIndex = reader.readU2();
|
|
390
512
|
const descriptorIndex = reader.readU2();
|
|
391
513
|
const attributesCount = reader.readU2();
|
|
392
|
-
const
|
|
514
|
+
const attributeResult = readAttributes(reader, cp, attributesCount);
|
|
393
515
|
members.push({
|
|
394
516
|
name: readUtf8(cp, nameIndex),
|
|
395
517
|
descriptor: readUtf8(cp, descriptorIndex),
|
|
396
518
|
accessFlags,
|
|
397
519
|
isSynthetic: (accessFlags & ACC_SYNTHETIC) !== 0 ||
|
|
398
|
-
|
|
520
|
+
attributeResult.names.some((attributeName) => attributeName === "Synthetic"),
|
|
521
|
+
...(attributeResult.annotationDefault !== undefined
|
|
522
|
+
? { annotationDefault: attributeResult.annotationDefault }
|
|
523
|
+
: {}),
|
|
524
|
+
...(attributeResult.annotations ? { annotations: attributeResult.annotations } : {})
|
|
399
525
|
});
|
|
400
526
|
}
|
|
401
527
|
return members;
|
|
@@ -478,6 +604,9 @@ export class MinecraftExplorerService {
|
|
|
478
604
|
const parsedClassCache = new Map([[parsed.internalName, parsed]]);
|
|
479
605
|
const warnings = [];
|
|
480
606
|
const warnMissingInheritedClass = (internalName, relation) => {
|
|
607
|
+
if (KNOWN_ABSENT_PLATFORM_PREFIXES.some((prefix) => internalName.startsWith(prefix))) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
481
610
|
warnings.push(`Could not resolve ${relation} class "${internalName.replace(/\//g, ".")}" while expanding inherited members.`);
|
|
482
611
|
};
|
|
483
612
|
const readParsedClassByInternalName = async (internalName, relation) => {
|
|
@@ -572,7 +701,9 @@ export class MinecraftExplorerService {
|
|
|
572
701
|
javaSignature: `${modifiers ? `${modifiers} ` : ""}${fieldType} ${member.name}`.trim(),
|
|
573
702
|
jvmDescriptor: member.descriptor,
|
|
574
703
|
accessFlags: member.accessFlags,
|
|
575
|
-
isSynthetic: member.isSynthetic
|
|
704
|
+
isSynthetic: member.isSynthetic,
|
|
705
|
+
...(member.annotationDefault !== undefined ? { annotationDefault: member.annotationDefault } : {}),
|
|
706
|
+
...(member.annotations ? { annotations: member.annotations } : {})
|
|
576
707
|
};
|
|
577
708
|
}
|
|
578
709
|
const parsedMethod = parseMethodDescriptor(member.descriptor);
|
|
@@ -585,7 +716,9 @@ export class MinecraftExplorerService {
|
|
|
585
716
|
javaSignature: `${modifiers ? `${modifiers} ` : ""}${ownerSimpleClassName}(${args})`.trim(),
|
|
586
717
|
jvmDescriptor: member.descriptor,
|
|
587
718
|
accessFlags: member.accessFlags,
|
|
588
|
-
isSynthetic: member.isSynthetic
|
|
719
|
+
isSynthetic: member.isSynthetic,
|
|
720
|
+
...(member.annotationDefault !== undefined ? { annotationDefault: member.annotationDefault } : {}),
|
|
721
|
+
...(member.annotations ? { annotations: member.annotations } : {})
|
|
589
722
|
};
|
|
590
723
|
}
|
|
591
724
|
return {
|
|
@@ -594,7 +727,9 @@ export class MinecraftExplorerService {
|
|
|
594
727
|
javaSignature: `${modifiers ? `${modifiers} ` : ""}${parsedMethod.returnType} ${member.name}(${args})`.trim(),
|
|
595
728
|
jvmDescriptor: member.descriptor,
|
|
596
729
|
accessFlags: member.accessFlags,
|
|
597
|
-
isSynthetic: member.isSynthetic
|
|
730
|
+
isSynthetic: member.isSynthetic,
|
|
731
|
+
...(member.annotationDefault !== undefined ? { annotationDefault: member.annotationDefault } : {}),
|
|
732
|
+
...(member.annotations ? { annotations: member.annotations } : {})
|
|
598
733
|
};
|
|
599
734
|
};
|
|
600
735
|
const shouldIncludeMember = (member) => {
|
|
@@ -652,10 +787,14 @@ export class MinecraftExplorerService {
|
|
|
652
787
|
};
|
|
653
788
|
}
|
|
654
789
|
contextForJar(jarPath) {
|
|
790
|
+
const minecraftVersion = extractVersionFromPath(jarPath);
|
|
655
791
|
return {
|
|
656
|
-
minecraftVersion:
|
|
792
|
+
minecraftVersion: minecraftVersion ?? "unknown",
|
|
657
793
|
mappingType: "unknown",
|
|
658
|
-
|
|
794
|
+
// Unobfuscated releases ship mojang names in their bytecode; claiming
|
|
795
|
+
// "obfuscated" for them misled namespace reconciliation downstream.
|
|
796
|
+
// With no derivable version the conservative "obfuscated" stands.
|
|
797
|
+
mappingNamespace: minecraftVersion && isUnobfuscatedVersion(minecraftVersion) ? "mojang" : "obfuscated",
|
|
659
798
|
jarHash: artifactSignatureFromFile(jarPath).sourceArtifactId,
|
|
660
799
|
generatedAt: new Date().toISOString()
|
|
661
800
|
};
|
package/dist/mod-analyzer.d.ts
CHANGED
|
@@ -19,8 +19,15 @@ export interface ModAnalysisResult {
|
|
|
19
19
|
dependencies?: ModDependency[];
|
|
20
20
|
classCount: number;
|
|
21
21
|
classes?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* In-archive paths of bundled Jar-in-Jar nested jars (META-INF/jars scan
|
|
24
|
+
* plus present-and-safe fabric.mod.json "jars" declarations). Absent when
|
|
25
|
+
* the mod bundles none.
|
|
26
|
+
*/
|
|
27
|
+
nestedJars?: string[];
|
|
22
28
|
}
|
|
23
29
|
export interface AnalyzeModOptions {
|
|
24
30
|
includeClasses?: boolean;
|
|
25
31
|
}
|
|
32
|
+
export declare function collectNestedJars(entries: string[], declared: string[] | undefined): string[];
|
|
26
33
|
export declare function analyzeModJar(jarPath: string, options?: AnalyzeModOptions): Promise<ModAnalysisResult>;
|
package/dist/mod-analyzer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parse as parseToml } from "smol-toml";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { AppError, ERROR_CODES } from "./errors.js";
|
|
4
|
-
import { normalizeJarPath } from "./path-resolver.js";
|
|
4
|
+
import { isSecureJarEntryPath, normalizeJarPath } from "./path-resolver.js";
|
|
5
5
|
import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
|
|
6
6
|
function toErrorMessage(value) {
|
|
7
7
|
if (value instanceof Error) {
|
|
@@ -32,7 +32,8 @@ const fabricModJsonSchema = z
|
|
|
32
32
|
depends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
|
|
33
33
|
recommends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
|
|
34
34
|
conflicts: z.record(z.union([z.string(), z.array(z.string())])).optional(),
|
|
35
|
-
suggests: z.record(z.union([z.string(), z.array(z.string())])).optional()
|
|
35
|
+
suggests: z.record(z.union([z.string(), z.array(z.string())])).optional(),
|
|
36
|
+
jars: z.array(z.object({ file: z.string() }).passthrough()).optional()
|
|
36
37
|
})
|
|
37
38
|
.passthrough();
|
|
38
39
|
const quiltModJsonSchema = z
|
|
@@ -120,6 +121,7 @@ function parseFabricMod(content) {
|
|
|
120
121
|
...collectFabricDeps(mod.conflicts, "conflicts"),
|
|
121
122
|
...collectFabricDeps(mod.suggests, "optional")
|
|
122
123
|
];
|
|
124
|
+
const declaredNestedJars = mod.jars?.map((entry) => entry.file);
|
|
123
125
|
return {
|
|
124
126
|
modId: mod.id,
|
|
125
127
|
modName: mod.name,
|
|
@@ -128,9 +130,25 @@ function parseFabricMod(content) {
|
|
|
128
130
|
entrypoints,
|
|
129
131
|
mixinConfigs,
|
|
130
132
|
accessWidener: mod.accessWidener,
|
|
131
|
-
dependencies: dependencies.length > 0 ? dependencies : undefined
|
|
133
|
+
dependencies: dependencies.length > 0 ? dependencies : undefined,
|
|
134
|
+
...(declaredNestedJars && declaredNestedJars.length > 0 ? { declaredNestedJars } : {})
|
|
132
135
|
};
|
|
133
136
|
}
|
|
137
|
+
// Nested-jar inventory: every real `.jar` entry directly under META-INF/jars/
|
|
138
|
+
// plus declared fabric.mod.json "jars" files that actually exist in the
|
|
139
|
+
// archive under a safe (non-escaping) path. Declared-but-absent or escaping
|
|
140
|
+
// paths are dropped — the inventory never names content the archive cannot
|
|
141
|
+
// safely serve.
|
|
142
|
+
export function collectNestedJars(entries, declared) {
|
|
143
|
+
const present = new Set(entries);
|
|
144
|
+
const collected = new Set(entries.filter((entry) => /^META-INF\/jars\/[^/]+\.jar$/.test(entry)));
|
|
145
|
+
for (const file of declared ?? []) {
|
|
146
|
+
if (isSecureJarEntryPath(file) && present.has(file) && file.endsWith(".jar")) {
|
|
147
|
+
collected.add(file);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [...collected].sort((left, right) => left.localeCompare(right));
|
|
151
|
+
}
|
|
134
152
|
// ---------------------------------------------------------------------------
|
|
135
153
|
// Parser: Quilt
|
|
136
154
|
// ---------------------------------------------------------------------------
|
|
@@ -350,20 +368,23 @@ export async function analyzeModJar(jarPath, options) {
|
|
|
350
368
|
// graceful fallback
|
|
351
369
|
}
|
|
352
370
|
}
|
|
371
|
+
const { declaredNestedJars, ...metadataRest } = metadata;
|
|
372
|
+
const nestedJars = collectNestedJars(entries, declaredNestedJars);
|
|
353
373
|
return {
|
|
354
374
|
loader,
|
|
355
375
|
jarKind,
|
|
356
|
-
...
|
|
357
|
-
...(packagedAccessTransformers.length > 0 ||
|
|
376
|
+
...metadataRest,
|
|
377
|
+
...(packagedAccessTransformers.length > 0 || metadataRest.accessTransformers
|
|
358
378
|
? {
|
|
359
379
|
accessTransformers: [...new Set([
|
|
360
|
-
...(
|
|
380
|
+
...(metadataRest.accessTransformers ?? []),
|
|
361
381
|
...packagedAccessTransformers
|
|
362
382
|
])]
|
|
363
383
|
}
|
|
364
384
|
: {}),
|
|
365
385
|
classCount,
|
|
366
|
-
...(classes !== undefined ? { classes } : {})
|
|
386
|
+
...(classes !== undefined ? { classes } : {}),
|
|
387
|
+
...(nestedJars.length > 0 ? { nestedJars } : {})
|
|
367
388
|
};
|
|
368
389
|
}
|
|
369
390
|
//# sourceMappingURL=mod-analyzer.js.map
|
|
@@ -7,6 +7,8 @@ export type VersionSourceDiscovery = {
|
|
|
7
7
|
candidateArtifacts: string[];
|
|
8
8
|
selectedSourceJarPath?: string;
|
|
9
9
|
selectedHasMinecraftNamespace?: boolean;
|
|
10
|
+
/** The other half of a Loom split-source pair (common/clientOnly), when present. */
|
|
11
|
+
companionSourceJarPaths?: string[];
|
|
10
12
|
};
|
|
11
13
|
type RuntimeJarCandidate = {
|
|
12
14
|
jarPath: string;
|