@adhisang/minecraft-modding-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/dist/access-widener-parser.d.ts +24 -0
- package/dist/access-widener-parser.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +178 -0
- package/dist/decompiler/vineflower.d.ts +15 -0
- package/dist/decompiler/vineflower.js +185 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.js +49 -0
- package/dist/hash.d.ts +1 -0
- package/dist/hash.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1447 -0
- package/dist/java-process.d.ts +16 -0
- package/dist/java-process.js +120 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +21 -0
- package/dist/mapping-pipeline-service.d.ts +18 -0
- package/dist/mapping-pipeline-service.js +60 -0
- package/dist/mapping-service.d.ts +161 -0
- package/dist/mapping-service.js +1706 -0
- package/dist/maven-resolver.d.ts +22 -0
- package/dist/maven-resolver.js +122 -0
- package/dist/minecraft-explorer-service.d.ts +43 -0
- package/dist/minecraft-explorer-service.js +562 -0
- package/dist/mixin-parser.d.ts +34 -0
- package/dist/mixin-parser.js +194 -0
- package/dist/mixin-validator.d.ts +59 -0
- package/dist/mixin-validator.js +274 -0
- package/dist/mod-analyzer.d.ts +23 -0
- package/dist/mod-analyzer.js +346 -0
- package/dist/mod-decompile-service.d.ts +39 -0
- package/dist/mod-decompile-service.js +136 -0
- package/dist/mod-remap-service.d.ts +17 -0
- package/dist/mod-remap-service.js +186 -0
- package/dist/mod-search-service.d.ts +28 -0
- package/dist/mod-search-service.js +174 -0
- package/dist/mojang-tiny-mapping-service.d.ts +13 -0
- package/dist/mojang-tiny-mapping-service.js +351 -0
- package/dist/nbt/java-nbt-codec.d.ts +3 -0
- package/dist/nbt/java-nbt-codec.js +385 -0
- package/dist/nbt/json-patch.d.ts +3 -0
- package/dist/nbt/json-patch.js +352 -0
- package/dist/nbt/pipeline.d.ts +39 -0
- package/dist/nbt/pipeline.js +173 -0
- package/dist/nbt/typed-json.d.ts +10 -0
- package/dist/nbt/typed-json.js +205 -0
- package/dist/nbt/types.d.ts +66 -0
- package/dist/nbt/types.js +2 -0
- package/dist/observability.d.ts +88 -0
- package/dist/observability.js +165 -0
- package/dist/path-converter.d.ts +12 -0
- package/dist/path-converter.js +161 -0
- package/dist/path-resolver.d.ts +19 -0
- package/dist/path-resolver.js +78 -0
- package/dist/registry-service.d.ts +29 -0
- package/dist/registry-service.js +214 -0
- package/dist/repo-downloader.d.ts +15 -0
- package/dist/repo-downloader.js +111 -0
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +154 -0
- package/dist/search-hit-accumulator.d.ts +38 -0
- package/dist/search-hit-accumulator.js +153 -0
- package/dist/source-jar-reader.d.ts +13 -0
- package/dist/source-jar-reader.js +216 -0
- package/dist/source-resolver.d.ts +14 -0
- package/dist/source-resolver.js +274 -0
- package/dist/source-service.d.ts +404 -0
- package/dist/source-service.js +2881 -0
- package/dist/storage/artifacts-repo.d.ts +45 -0
- package/dist/storage/artifacts-repo.js +209 -0
- package/dist/storage/db.d.ts +14 -0
- package/dist/storage/db.js +132 -0
- package/dist/storage/files-repo.d.ts +78 -0
- package/dist/storage/files-repo.js +437 -0
- package/dist/storage/index-meta-repo.d.ts +35 -0
- package/dist/storage/index-meta-repo.js +97 -0
- package/dist/storage/migrations.d.ts +11 -0
- package/dist/storage/migrations.js +71 -0
- package/dist/storage/schema.d.ts +1 -0
- package/dist/storage/schema.js +160 -0
- package/dist/storage/sqlite.d.ts +20 -0
- package/dist/storage/sqlite.js +111 -0
- package/dist/storage/symbols-repo.d.ts +63 -0
- package/dist/storage/symbols-repo.js +401 -0
- package/dist/symbols/symbol-extractor.d.ts +7 -0
- package/dist/symbols/symbol-extractor.js +64 -0
- package/dist/tiny-remapper-resolver.d.ts +1 -0
- package/dist/tiny-remapper-resolver.js +62 -0
- package/dist/tiny-remapper-service.d.ts +16 -0
- package/dist/tiny-remapper-service.js +73 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +2 -0
- package/dist/version-diff-service.d.ts +41 -0
- package/dist/version-diff-service.js +222 -0
- package/dist/version-service.d.ts +70 -0
- package/dist/version-service.js +411 -0
- package/dist/vineflower-resolver.d.ts +1 -0
- package/dist/vineflower-resolver.js +62 -0
- package/dist/workspace-mapping-service.d.ts +18 -0
- package/dist/workspace-mapping-service.js +89 -0
- package/package.json +61 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { parse as parseToml } from "smol-toml";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { AppError, ERROR_CODES } from "./errors.js";
|
|
4
|
+
import { normalizeJarPath } from "./path-resolver.js";
|
|
5
|
+
import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
|
|
6
|
+
function toErrorMessage(value) {
|
|
7
|
+
if (value instanceof Error) {
|
|
8
|
+
return value.message;
|
|
9
|
+
}
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Zod schemas — .passthrough() for lenient parsing
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
const stringOrEntrypoint = z.union([
|
|
16
|
+
z.string(),
|
|
17
|
+
z.object({ value: z.string() }).passthrough()
|
|
18
|
+
]);
|
|
19
|
+
const stringOrMixinRef = z.union([
|
|
20
|
+
z.string(),
|
|
21
|
+
z.object({ config: z.string() }).passthrough()
|
|
22
|
+
]);
|
|
23
|
+
const fabricModJsonSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
id: z.string().optional(),
|
|
26
|
+
name: z.string().optional(),
|
|
27
|
+
version: z.string().optional(),
|
|
28
|
+
description: z.string().optional(),
|
|
29
|
+
entrypoints: z.record(z.array(stringOrEntrypoint)).optional(),
|
|
30
|
+
mixins: z.array(stringOrMixinRef).optional(),
|
|
31
|
+
accessWidener: z.string().optional(),
|
|
32
|
+
depends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
|
|
33
|
+
recommends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
|
|
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()
|
|
36
|
+
})
|
|
37
|
+
.passthrough();
|
|
38
|
+
const quiltModJsonSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
schema_version: z.number().optional(),
|
|
41
|
+
quilt_loader: z
|
|
42
|
+
.object({
|
|
43
|
+
id: z.string().optional(),
|
|
44
|
+
version: z.string().optional(),
|
|
45
|
+
metadata: z
|
|
46
|
+
.object({
|
|
47
|
+
name: z.string().optional(),
|
|
48
|
+
description: z.string().optional()
|
|
49
|
+
})
|
|
50
|
+
.passthrough()
|
|
51
|
+
.optional(),
|
|
52
|
+
entrypoints: z.record(z.array(stringOrEntrypoint)).optional(),
|
|
53
|
+
depends: z.array(z.unknown()).optional()
|
|
54
|
+
})
|
|
55
|
+
.passthrough()
|
|
56
|
+
.optional(),
|
|
57
|
+
mixin: z.union([z.string(), z.array(z.string())]).optional(),
|
|
58
|
+
access_widener: z.string().optional()
|
|
59
|
+
})
|
|
60
|
+
.passthrough();
|
|
61
|
+
const forgeModsTomlSchema = z
|
|
62
|
+
.object({
|
|
63
|
+
modLoader: z.string().optional(),
|
|
64
|
+
mods: z
|
|
65
|
+
.array(z
|
|
66
|
+
.object({
|
|
67
|
+
modId: z.string().optional(),
|
|
68
|
+
displayName: z.string().optional(),
|
|
69
|
+
version: z.string().optional(),
|
|
70
|
+
description: z.string().optional()
|
|
71
|
+
})
|
|
72
|
+
.passthrough())
|
|
73
|
+
.optional(),
|
|
74
|
+
dependencies: z.record(z.array(z.unknown())).optional(),
|
|
75
|
+
mixins: z.array(z.object({ config: z.string() }).passthrough()).optional()
|
|
76
|
+
})
|
|
77
|
+
.passthrough();
|
|
78
|
+
const legacyForgeSchema = z.array(z
|
|
79
|
+
.object({
|
|
80
|
+
modid: z.string().optional(),
|
|
81
|
+
name: z.string().optional(),
|
|
82
|
+
version: z.string().optional(),
|
|
83
|
+
description: z.string().optional()
|
|
84
|
+
})
|
|
85
|
+
.passthrough());
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Parser: Fabric
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
function normalizeFabricEntrypoint(v) {
|
|
90
|
+
return typeof v === "string" ? v : v.value;
|
|
91
|
+
}
|
|
92
|
+
function normalizeMixinRef(v) {
|
|
93
|
+
return typeof v === "string" ? v : v.config;
|
|
94
|
+
}
|
|
95
|
+
function collectFabricDeps(record, kind) {
|
|
96
|
+
if (!record)
|
|
97
|
+
return [];
|
|
98
|
+
return Object.entries(record).map(([modId, versionRange]) => ({
|
|
99
|
+
modId,
|
|
100
|
+
versionRange: Array.isArray(versionRange) ? versionRange.join(" || ") : versionRange,
|
|
101
|
+
kind
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
function parseFabricMod(content) {
|
|
105
|
+
const parsed = fabricModJsonSchema.safeParse(JSON.parse(content));
|
|
106
|
+
if (!parsed.success)
|
|
107
|
+
return {};
|
|
108
|
+
const mod = parsed.data;
|
|
109
|
+
const entrypoints = mod.entrypoints
|
|
110
|
+
? Object.fromEntries(Object.entries(mod.entrypoints).map(([key, values]) => [
|
|
111
|
+
key,
|
|
112
|
+
values.map(normalizeFabricEntrypoint)
|
|
113
|
+
]))
|
|
114
|
+
: undefined;
|
|
115
|
+
const mixinConfigs = mod.mixins?.map(normalizeMixinRef);
|
|
116
|
+
const dependencies = [
|
|
117
|
+
...collectFabricDeps(mod.depends, "required"),
|
|
118
|
+
...collectFabricDeps(mod.recommends, "recommends"),
|
|
119
|
+
...collectFabricDeps(mod.conflicts, "conflicts"),
|
|
120
|
+
...collectFabricDeps(mod.suggests, "optional")
|
|
121
|
+
];
|
|
122
|
+
return {
|
|
123
|
+
modId: mod.id,
|
|
124
|
+
modName: mod.name,
|
|
125
|
+
modVersion: mod.version,
|
|
126
|
+
description: mod.description,
|
|
127
|
+
entrypoints,
|
|
128
|
+
mixinConfigs,
|
|
129
|
+
accessWidener: mod.accessWidener,
|
|
130
|
+
dependencies: dependencies.length > 0 ? dependencies : undefined
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Parser: Quilt
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
function parseQuiltMod(content) {
|
|
137
|
+
const parsed = quiltModJsonSchema.safeParse(JSON.parse(content));
|
|
138
|
+
if (!parsed.success)
|
|
139
|
+
return {};
|
|
140
|
+
const mod = parsed.data;
|
|
141
|
+
const loader = mod.quilt_loader;
|
|
142
|
+
const entrypoints = loader?.entrypoints
|
|
143
|
+
? Object.fromEntries(Object.entries(loader.entrypoints).map(([key, values]) => [
|
|
144
|
+
key,
|
|
145
|
+
values.map(normalizeFabricEntrypoint)
|
|
146
|
+
]))
|
|
147
|
+
: undefined;
|
|
148
|
+
const rawMixin = mod.mixin;
|
|
149
|
+
const mixinConfigs = rawMixin
|
|
150
|
+
? Array.isArray(rawMixin)
|
|
151
|
+
? rawMixin
|
|
152
|
+
: [rawMixin]
|
|
153
|
+
: undefined;
|
|
154
|
+
const dependencies = [];
|
|
155
|
+
if (loader?.depends && Array.isArray(loader.depends)) {
|
|
156
|
+
for (const dep of loader.depends) {
|
|
157
|
+
if (typeof dep === "string") {
|
|
158
|
+
dependencies.push({ modId: dep, kind: "required" });
|
|
159
|
+
}
|
|
160
|
+
else if (dep && typeof dep === "object" && "id" in dep) {
|
|
161
|
+
const d = dep;
|
|
162
|
+
dependencies.push({
|
|
163
|
+
modId: d.id,
|
|
164
|
+
versionRange: d.versions,
|
|
165
|
+
kind: d.optional ? "optional" : "required"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
modId: loader?.id,
|
|
172
|
+
modName: loader?.metadata?.name,
|
|
173
|
+
modVersion: loader?.version,
|
|
174
|
+
description: loader?.metadata?.description,
|
|
175
|
+
entrypoints,
|
|
176
|
+
mixinConfigs,
|
|
177
|
+
accessWidener: mod.access_widener,
|
|
178
|
+
dependencies: dependencies.length > 0 ? dependencies : undefined
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Parser: Forge / NeoForge (TOML)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
function parseForgeMod(content, entries) {
|
|
185
|
+
const raw = parseToml(content);
|
|
186
|
+
const parsed = forgeModsTomlSchema.safeParse(raw);
|
|
187
|
+
if (!parsed.success)
|
|
188
|
+
return {};
|
|
189
|
+
const toml = parsed.data;
|
|
190
|
+
const firstMod = toml.mods?.[0];
|
|
191
|
+
// Determine Forge vs NeoForge from modLoader field
|
|
192
|
+
const modLoaderValue = toml.modLoader?.toLowerCase() ?? "";
|
|
193
|
+
const isNeoForge = modLoaderValue.includes("neoforge") || modLoaderValue.includes("lowcodefml");
|
|
194
|
+
// Also check for neoforge.mods.toml presence
|
|
195
|
+
const hasNeoforgeToml = entries.includes("META-INF/neoforge.mods.toml");
|
|
196
|
+
const detectedLoader = isNeoForge || hasNeoforgeToml ? "neoforge" : "forge";
|
|
197
|
+
// Dependencies
|
|
198
|
+
const dependencies = [];
|
|
199
|
+
if (toml.dependencies) {
|
|
200
|
+
for (const depArray of Object.values(toml.dependencies)) {
|
|
201
|
+
for (const dep of depArray) {
|
|
202
|
+
if (dep && typeof dep === "object") {
|
|
203
|
+
const d = dep;
|
|
204
|
+
if (d.modId) {
|
|
205
|
+
dependencies.push({
|
|
206
|
+
modId: d.modId,
|
|
207
|
+
versionRange: d.versionRange,
|
|
208
|
+
kind: d.mandatory === false ? "optional" : "required"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Mixin configs
|
|
216
|
+
const mixinConfigs = toml.mixins?.map((m) => m.config);
|
|
217
|
+
return {
|
|
218
|
+
detectedLoader,
|
|
219
|
+
modId: firstMod?.modId,
|
|
220
|
+
modName: firstMod?.displayName,
|
|
221
|
+
modVersion: firstMod?.version,
|
|
222
|
+
description: firstMod?.description,
|
|
223
|
+
mixinConfigs: mixinConfigs && mixinConfigs.length > 0 ? mixinConfigs : undefined,
|
|
224
|
+
dependencies: dependencies.length > 0 ? dependencies : undefined
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Parser: Legacy Forge (mcmod.info)
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
function parseLegacyForgeMod(content) {
|
|
231
|
+
// mcmod.info may be wrapped in an extra array or be a direct array
|
|
232
|
+
let rawArray = JSON.parse(content);
|
|
233
|
+
if (Array.isArray(rawArray) && rawArray.length === 1 && Array.isArray(rawArray[0])) {
|
|
234
|
+
rawArray = rawArray[0];
|
|
235
|
+
}
|
|
236
|
+
const parsed = legacyForgeSchema.safeParse(rawArray);
|
|
237
|
+
if (!parsed.success)
|
|
238
|
+
return {};
|
|
239
|
+
const first = parsed.data[0];
|
|
240
|
+
if (!first)
|
|
241
|
+
return {};
|
|
242
|
+
return {
|
|
243
|
+
modId: first.modid,
|
|
244
|
+
modName: first.name,
|
|
245
|
+
modVersion: first.version,
|
|
246
|
+
description: first.description
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Main export
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
export async function analyzeModJar(jarPath, options) {
|
|
253
|
+
let resolvedPath;
|
|
254
|
+
try {
|
|
255
|
+
resolvedPath = normalizeJarPath(jarPath);
|
|
256
|
+
}
|
|
257
|
+
catch (cause) {
|
|
258
|
+
throw new AppError({
|
|
259
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
260
|
+
message: cause instanceof Error ? cause.message : `Invalid jar path: ${jarPath}`,
|
|
261
|
+
details: { jarPath }
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
let entries;
|
|
265
|
+
try {
|
|
266
|
+
entries = await listJarEntries(resolvedPath);
|
|
267
|
+
}
|
|
268
|
+
catch (cause) {
|
|
269
|
+
throw new AppError({
|
|
270
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
271
|
+
message: `Failed to read jar "${resolvedPath}".`,
|
|
272
|
+
details: {
|
|
273
|
+
jarPath: resolvedPath,
|
|
274
|
+
reason: toErrorMessage(cause)
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// Class counting
|
|
279
|
+
const classEntries = entries.filter((e) => e.endsWith(".class"));
|
|
280
|
+
const classCount = classEntries.length;
|
|
281
|
+
const classes = options?.includeClasses ? classEntries : undefined;
|
|
282
|
+
// Detect loader and parse metadata
|
|
283
|
+
let loader = "unknown";
|
|
284
|
+
let metadata = {};
|
|
285
|
+
if (entries.includes("fabric.mod.json")) {
|
|
286
|
+
loader = "fabric";
|
|
287
|
+
try {
|
|
288
|
+
const content = await readJarEntryAsUtf8(resolvedPath, "fabric.mod.json");
|
|
289
|
+
metadata = parseFabricMod(content);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// graceful fallback
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (entries.includes("quilt.mod.json")) {
|
|
296
|
+
loader = "quilt";
|
|
297
|
+
try {
|
|
298
|
+
const content = await readJarEntryAsUtf8(resolvedPath, "quilt.mod.json");
|
|
299
|
+
metadata = parseQuiltMod(content);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// graceful fallback
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else if (entries.includes("META-INF/neoforge.mods.toml")) {
|
|
306
|
+
loader = "neoforge";
|
|
307
|
+
try {
|
|
308
|
+
const content = await readJarEntryAsUtf8(resolvedPath, "META-INF/neoforge.mods.toml");
|
|
309
|
+
const result = parseForgeMod(content, entries);
|
|
310
|
+
const { detectedLoader: _, ...rest } = result;
|
|
311
|
+
metadata = rest;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// graceful fallback
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (entries.includes("META-INF/mods.toml")) {
|
|
318
|
+
try {
|
|
319
|
+
const content = await readJarEntryAsUtf8(resolvedPath, "META-INF/mods.toml");
|
|
320
|
+
const result = parseForgeMod(content, entries);
|
|
321
|
+
loader = result.detectedLoader ?? "forge";
|
|
322
|
+
const { detectedLoader: _, ...rest } = result;
|
|
323
|
+
metadata = rest;
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
loader = "forge";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else if (entries.includes("mcmod.info")) {
|
|
330
|
+
loader = "forge";
|
|
331
|
+
try {
|
|
332
|
+
const content = await readJarEntryAsUtf8(resolvedPath, "mcmod.info");
|
|
333
|
+
metadata = parseLegacyForgeMod(content);
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
// graceful fallback
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
loader,
|
|
341
|
+
...metadata,
|
|
342
|
+
classCount,
|
|
343
|
+
...(classes !== undefined ? { classes } : {})
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
//# sourceMappingURL=mod-analyzer.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Config } from "./types.js";
|
|
2
|
+
export type DecompileModJarInput = {
|
|
3
|
+
jarPath: string;
|
|
4
|
+
className?: string;
|
|
5
|
+
};
|
|
6
|
+
export type DecompileModJarOutput = {
|
|
7
|
+
modId: string;
|
|
8
|
+
modName?: string;
|
|
9
|
+
modVersion?: string;
|
|
10
|
+
loader: string;
|
|
11
|
+
outputDir: string;
|
|
12
|
+
fileCount: number;
|
|
13
|
+
files?: string[];
|
|
14
|
+
source?: {
|
|
15
|
+
className: string;
|
|
16
|
+
content: string;
|
|
17
|
+
totalLines: number;
|
|
18
|
+
};
|
|
19
|
+
warnings: string[];
|
|
20
|
+
};
|
|
21
|
+
export type GetModClassSourceInput = {
|
|
22
|
+
jarPath: string;
|
|
23
|
+
className: string;
|
|
24
|
+
};
|
|
25
|
+
export type GetModClassSourceOutput = {
|
|
26
|
+
className: string;
|
|
27
|
+
content: string;
|
|
28
|
+
totalLines: number;
|
|
29
|
+
modId?: string;
|
|
30
|
+
warnings: string[];
|
|
31
|
+
};
|
|
32
|
+
export declare class ModDecompileService {
|
|
33
|
+
private readonly config;
|
|
34
|
+
private readonly decompileCache;
|
|
35
|
+
constructor(config: Config);
|
|
36
|
+
decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
|
|
37
|
+
getModClassSource(input: GetModClassSourceInput): Promise<GetModClassSourceOutput>;
|
|
38
|
+
private ensureDecompiled;
|
|
39
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createError, ERROR_CODES } from "./errors.js";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
import { decompileBinaryJar } from "./decompiler/vineflower.js";
|
|
7
|
+
import { resolveVineflowerJar } from "./vineflower-resolver.js";
|
|
8
|
+
import { analyzeModJar } from "./mod-analyzer.js";
|
|
9
|
+
import { validateAndNormalizeJarPath } from "./path-resolver.js";
|
|
10
|
+
const DECOMPILE_TIMEOUT_MS = 300_000;
|
|
11
|
+
function modDecompileCacheKey(jarPath) {
|
|
12
|
+
return createHash("sha256").update(jarPath).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
function classNameToFilePath(className) {
|
|
15
|
+
return className.replaceAll(".", "/") + ".java";
|
|
16
|
+
}
|
|
17
|
+
function filePathToClassName(filePath) {
|
|
18
|
+
return filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
19
|
+
}
|
|
20
|
+
export class ModDecompileService {
|
|
21
|
+
config;
|
|
22
|
+
// Cache: jarPath hash → decompiled output dir + file list
|
|
23
|
+
decompileCache = new Map();
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
async decompileModJar(input) {
|
|
28
|
+
const jarPath = validateAndNormalizeJarPath(input.jarPath);
|
|
29
|
+
const warnings = [];
|
|
30
|
+
const { outputDir, files, analysis } = await this.ensureDecompiled(jarPath, warnings);
|
|
31
|
+
let sourceResult;
|
|
32
|
+
if (input.className) {
|
|
33
|
+
const targetFile = classNameToFilePath(input.className);
|
|
34
|
+
const matched = files.find((f) => f === targetFile || f.endsWith(`/${targetFile}`) || f === input.className);
|
|
35
|
+
if (matched) {
|
|
36
|
+
const content = readFileSync(join(outputDir, matched), "utf8");
|
|
37
|
+
sourceResult = {
|
|
38
|
+
className: filePathToClassName(matched),
|
|
39
|
+
content,
|
|
40
|
+
totalLines: content.split("\n").length
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
warnings.push(`Class "${input.className}" not found in decompiled output. Use the files list to find available classes.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
modId: analysis.modId ?? "unknown",
|
|
49
|
+
modName: analysis.modName,
|
|
50
|
+
modVersion: analysis.modVersion,
|
|
51
|
+
loader: analysis.loader,
|
|
52
|
+
outputDir,
|
|
53
|
+
fileCount: files.length,
|
|
54
|
+
files: input.className ? undefined : files.map(filePathToClassName),
|
|
55
|
+
source: sourceResult,
|
|
56
|
+
warnings
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async getModClassSource(input) {
|
|
60
|
+
const jarPath = validateAndNormalizeJarPath(input.jarPath);
|
|
61
|
+
const className = input.className.trim();
|
|
62
|
+
if (!className) {
|
|
63
|
+
throw createError({
|
|
64
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
65
|
+
message: "className must be non-empty."
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const warnings = [];
|
|
69
|
+
const { outputDir, files, analysis } = await this.ensureDecompiled(jarPath, warnings);
|
|
70
|
+
const targetFile = classNameToFilePath(className);
|
|
71
|
+
const matched = files.find((f) => f === targetFile || f.endsWith(`/${targetFile}`));
|
|
72
|
+
if (!matched) {
|
|
73
|
+
throw createError({
|
|
74
|
+
code: ERROR_CODES.CLASS_NOT_FOUND,
|
|
75
|
+
message: `Class "${className}" not found in decompiled mod JAR.`,
|
|
76
|
+
details: { className, jarPath, availableCount: files.length }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const content = readFileSync(join(outputDir, matched), "utf8");
|
|
80
|
+
return {
|
|
81
|
+
className: filePathToClassName(matched),
|
|
82
|
+
content,
|
|
83
|
+
totalLines: content.split("\n").length,
|
|
84
|
+
modId: analysis.modId,
|
|
85
|
+
warnings
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
async ensureDecompiled(jarPath, warnings) {
|
|
89
|
+
const cacheKey = modDecompileCacheKey(jarPath);
|
|
90
|
+
const cached = this.decompileCache.get(cacheKey);
|
|
91
|
+
if (cached)
|
|
92
|
+
return cached;
|
|
93
|
+
log("info", "mod-decompile.start", { jarPath });
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
// Analyze mod metadata
|
|
96
|
+
let analysis;
|
|
97
|
+
try {
|
|
98
|
+
analysis = await analyzeModJar(jarPath, { includeClasses: false });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
analysis = {
|
|
102
|
+
loader: "unknown",
|
|
103
|
+
classCount: 0
|
|
104
|
+
};
|
|
105
|
+
warnings.push("Could not extract mod metadata from JAR.");
|
|
106
|
+
}
|
|
107
|
+
// Resolve Vineflower
|
|
108
|
+
const vineflowerPath = await resolveVineflowerJar(this.config.cacheDir, this.config.vineflowerJarPath);
|
|
109
|
+
// Decompile
|
|
110
|
+
const decompileResult = await decompileBinaryJar(jarPath, this.config.cacheDir, {
|
|
111
|
+
vineflowerJarPath: vineflowerPath,
|
|
112
|
+
timeoutMs: DECOMPILE_TIMEOUT_MS,
|
|
113
|
+
signature: cacheKey
|
|
114
|
+
});
|
|
115
|
+
const files = decompileResult.javaFiles.map((entry) => entry.filePath);
|
|
116
|
+
const result = {
|
|
117
|
+
outputDir: decompileResult.outputDir,
|
|
118
|
+
files,
|
|
119
|
+
analysis
|
|
120
|
+
};
|
|
121
|
+
this.decompileCache.set(cacheKey, result);
|
|
122
|
+
// Trim cache
|
|
123
|
+
if (this.decompileCache.size > 8) {
|
|
124
|
+
const oldest = this.decompileCache.keys().next().value;
|
|
125
|
+
if (oldest !== undefined)
|
|
126
|
+
this.decompileCache.delete(oldest);
|
|
127
|
+
}
|
|
128
|
+
log("info", "mod-decompile.done", {
|
|
129
|
+
jarPath,
|
|
130
|
+
fileCount: files.length,
|
|
131
|
+
durationMs: Date.now() - startedAt
|
|
132
|
+
});
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=mod-decompile-service.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Config } from "./types.js";
|
|
2
|
+
export interface ModRemapInput {
|
|
3
|
+
inputJar: string;
|
|
4
|
+
outputJar?: string;
|
|
5
|
+
mcVersion?: string;
|
|
6
|
+
targetMapping: "yarn" | "mojang";
|
|
7
|
+
}
|
|
8
|
+
export interface ModRemapResult {
|
|
9
|
+
outputJar: string;
|
|
10
|
+
mcVersion: string;
|
|
11
|
+
fromMapping: string;
|
|
12
|
+
targetMapping: string;
|
|
13
|
+
resolvedTargetNamespace: "yarn" | "mojang";
|
|
14
|
+
durationMs: number;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function remapModJar(input: ModRemapInput, config: Config): Promise<ModRemapResult>;
|