@adhisang/minecraft-modding-mcp 3.1.0 → 3.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 +62 -34
- package/README.md +79 -100
- package/dist/access-transformer-parser.d.ts +17 -0
- package/dist/access-transformer-parser.js +97 -0
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +24 -0
- package/dist/config.js +19 -11
- package/dist/decompiler/vineflower.js +22 -21
- package/dist/entry-tools/analyze-mod-service.d.ts +4 -4
- package/dist/entry-tools/analyze-symbol-service.d.ts +22 -20
- package/dist/entry-tools/analyze-symbol-service.js +6 -3
- package/dist/entry-tools/inspect-minecraft-service.d.ts +166 -149
- package/dist/entry-tools/inspect-minecraft-service.js +318 -55
- package/dist/entry-tools/validate-project-service.d.ts +153 -16
- package/dist/entry-tools/validate-project-service.js +360 -23
- package/dist/gradle-paths.d.ts +4 -0
- package/dist/gradle-paths.js +57 -0
- package/dist/index.js +274 -13
- package/dist/mapping-pipeline-service.d.ts +3 -1
- package/dist/mapping-pipeline-service.js +16 -1
- package/dist/mapping-service.d.ts +5 -0
- package/dist/mapping-service.js +200 -84
- package/dist/minecraft-explorer-service.d.ts +13 -0
- package/dist/minecraft-explorer-service.js +8 -4
- package/dist/mixin-validator.d.ts +33 -2
- package/dist/mixin-validator.js +197 -11
- package/dist/mod-analyzer.d.ts +1 -0
- package/dist/mod-analyzer.js +17 -1
- package/dist/mod-decompile-service.js +4 -4
- package/dist/mod-remap-service.js +1 -54
- package/dist/mod-search-service.d.ts +1 -0
- package/dist/mod-search-service.js +84 -51
- package/dist/response-utils.d.ts +35 -0
- package/dist/response-utils.js +113 -0
- package/dist/source-jar-reader.d.ts +16 -0
- package/dist/source-jar-reader.js +103 -1
- package/dist/source-resolver.js +9 -10
- package/dist/source-service.d.ts +24 -2
- package/dist/source-service.js +1052 -139
- package/dist/tool-contract-manifest.js +74 -74
- package/dist/types.d.ts +17 -0
- package/dist/workspace-mapping-service.d.ts +13 -0
- package/dist/workspace-mapping-service.js +146 -14
- package/package.json +1 -1
package/dist/mixin-validator.js
CHANGED
|
@@ -133,6 +133,21 @@ function allMethodNames(members) {
|
|
|
133
133
|
function allFieldNames(members) {
|
|
134
134
|
return members.fields.map((m) => m.name);
|
|
135
135
|
}
|
|
136
|
+
function accessLevelFromFlags(accessFlags) {
|
|
137
|
+
if (accessFlags == null) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if ((accessFlags & 0x0001) !== 0) {
|
|
141
|
+
return "public";
|
|
142
|
+
}
|
|
143
|
+
if ((accessFlags & 0x0004) !== 0) {
|
|
144
|
+
return "protected";
|
|
145
|
+
}
|
|
146
|
+
if ((accessFlags & 0x0002) !== 0) {
|
|
147
|
+
return "private";
|
|
148
|
+
}
|
|
149
|
+
return "package-private";
|
|
150
|
+
}
|
|
136
151
|
function computeFalsePositiveRisk(healthReport, resolutionPath, issueConfidence) {
|
|
137
152
|
if (!healthReport)
|
|
138
153
|
return undefined;
|
|
@@ -735,7 +750,7 @@ export function validateParsedMixin(parsed, targetMembers, warnings, provenance,
|
|
|
735
750
|
/* ------------------------------------------------------------------ */
|
|
736
751
|
/* Access Widener validation */
|
|
737
752
|
/* ------------------------------------------------------------------ */
|
|
738
|
-
export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
753
|
+
export function validateParsedAccessWidener(parsed, membersByClass, warnings, options) {
|
|
739
754
|
warnings.push(...parsed.parseWarnings);
|
|
740
755
|
const validatedEntries = [];
|
|
741
756
|
let validCount = 0;
|
|
@@ -743,15 +758,29 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
|
743
758
|
for (const entry of parsed.entries) {
|
|
744
759
|
const ownerFqn = entry.target.replace(/\//g, ".");
|
|
745
760
|
if (entry.targetKind === "class") {
|
|
746
|
-
|
|
747
|
-
|
|
761
|
+
const members = membersByClass.get(ownerFqn);
|
|
762
|
+
if (members) {
|
|
763
|
+
const runtimeAccess = accessLevelFromFlags(members.classAccessFlags);
|
|
764
|
+
validatedEntries.push({
|
|
765
|
+
...entry,
|
|
766
|
+
valid: true,
|
|
767
|
+
...(options?.includeRuntimeEvidence
|
|
768
|
+
? {
|
|
769
|
+
resolvedInRuntime: true,
|
|
770
|
+
...(runtimeAccess
|
|
771
|
+
? { resolvedRuntimeAccess: runtimeAccess }
|
|
772
|
+
: {})
|
|
773
|
+
}
|
|
774
|
+
: {})
|
|
775
|
+
});
|
|
748
776
|
validCount++;
|
|
749
777
|
}
|
|
750
778
|
else {
|
|
751
779
|
validatedEntries.push({
|
|
752
780
|
...entry,
|
|
753
781
|
valid: false,
|
|
754
|
-
issue: `Class "${ownerFqn}" not found in game jar
|
|
782
|
+
issue: `Class "${ownerFqn}" not found in game jar.`,
|
|
783
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
755
784
|
});
|
|
756
785
|
invalidCount++;
|
|
757
786
|
}
|
|
@@ -763,16 +792,37 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
|
763
792
|
validatedEntries.push({
|
|
764
793
|
...entry,
|
|
765
794
|
valid: false,
|
|
766
|
-
issue: `Owner class "${ownerFqn}" not found in game jar
|
|
795
|
+
issue: `Owner class "${ownerFqn}" not found in game jar.`,
|
|
796
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
767
797
|
});
|
|
768
798
|
invalidCount++;
|
|
769
799
|
continue;
|
|
770
800
|
}
|
|
771
801
|
if (entry.targetKind === "method") {
|
|
772
802
|
const methodNames = allMethodNames(members);
|
|
773
|
-
const
|
|
803
|
+
const matchedMember = members.methods.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor)) ?? members.constructors.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
|
|
804
|
+
const found = matchedMember != null;
|
|
774
805
|
if (found) {
|
|
775
|
-
|
|
806
|
+
const runtimeMember = matchedMember;
|
|
807
|
+
const runtimeAccess = accessLevelFromFlags(runtimeMember.accessFlags);
|
|
808
|
+
validatedEntries.push({
|
|
809
|
+
...entry,
|
|
810
|
+
valid: true,
|
|
811
|
+
...(options?.includeRuntimeEvidence
|
|
812
|
+
? {
|
|
813
|
+
resolvedInRuntime: true,
|
|
814
|
+
...(runtimeAccess
|
|
815
|
+
? { resolvedRuntimeAccess: runtimeAccess }
|
|
816
|
+
: {}),
|
|
817
|
+
...(runtimeMember.jvmDescriptor
|
|
818
|
+
? { resolvedRuntimeJvmDescriptor: runtimeMember.jvmDescriptor }
|
|
819
|
+
: {}),
|
|
820
|
+
...(runtimeMember.javaSignature
|
|
821
|
+
? { resolvedRuntimeJavaSignature: runtimeMember.javaSignature }
|
|
822
|
+
: {})
|
|
823
|
+
}
|
|
824
|
+
: {})
|
|
825
|
+
});
|
|
776
826
|
validCount++;
|
|
777
827
|
}
|
|
778
828
|
else {
|
|
@@ -781,7 +831,8 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
|
781
831
|
...entry,
|
|
782
832
|
valid: false,
|
|
783
833
|
issue: `Method "${entry.name}" not found in class "${ownerFqn}".`,
|
|
784
|
-
suggestions: suggestions.length > 0 ? suggestions : undefined
|
|
834
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
835
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
785
836
|
});
|
|
786
837
|
invalidCount++;
|
|
787
838
|
}
|
|
@@ -789,9 +840,29 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
|
789
840
|
else {
|
|
790
841
|
// field
|
|
791
842
|
const fieldNames = allFieldNames(members);
|
|
792
|
-
const
|
|
843
|
+
const matchedMember = members.fields.find((m) => m.name === entry.name && (!entry.descriptor || m.jvmDescriptor === entry.descriptor));
|
|
844
|
+
const found = matchedMember != null;
|
|
793
845
|
if (found) {
|
|
794
|
-
|
|
846
|
+
const runtimeMember = matchedMember;
|
|
847
|
+
const runtimeAccess = accessLevelFromFlags(runtimeMember.accessFlags);
|
|
848
|
+
validatedEntries.push({
|
|
849
|
+
...entry,
|
|
850
|
+
valid: true,
|
|
851
|
+
...(options?.includeRuntimeEvidence
|
|
852
|
+
? {
|
|
853
|
+
resolvedInRuntime: true,
|
|
854
|
+
...(runtimeAccess
|
|
855
|
+
? { resolvedRuntimeAccess: runtimeAccess }
|
|
856
|
+
: {}),
|
|
857
|
+
...(runtimeMember.jvmDescriptor
|
|
858
|
+
? { resolvedRuntimeJvmDescriptor: runtimeMember.jvmDescriptor }
|
|
859
|
+
: {}),
|
|
860
|
+
...(runtimeMember.javaSignature
|
|
861
|
+
? { resolvedRuntimeJavaSignature: runtimeMember.javaSignature }
|
|
862
|
+
: {})
|
|
863
|
+
}
|
|
864
|
+
: {})
|
|
865
|
+
});
|
|
795
866
|
validCount++;
|
|
796
867
|
}
|
|
797
868
|
else {
|
|
@@ -800,7 +871,8 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
|
800
871
|
...entry,
|
|
801
872
|
valid: false,
|
|
802
873
|
issue: `Field "${entry.name}" not found in class "${ownerFqn}".`,
|
|
803
|
-
suggestions: suggestions.length > 0 ? suggestions : undefined
|
|
874
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
875
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
804
876
|
});
|
|
805
877
|
invalidCount++;
|
|
806
878
|
}
|
|
@@ -819,4 +891,118 @@ export function validateParsedAccessWidener(parsed, membersByClass, warnings) {
|
|
|
819
891
|
warnings
|
|
820
892
|
};
|
|
821
893
|
}
|
|
894
|
+
export function validateParsedAccessTransformer(parsed, membersByClass, warnings, options) {
|
|
895
|
+
warnings.push(...parsed.parseWarnings);
|
|
896
|
+
const validatedEntries = [];
|
|
897
|
+
let validCount = 0;
|
|
898
|
+
let invalidCount = 0;
|
|
899
|
+
for (const entry of parsed.entries) {
|
|
900
|
+
const ownerFqn = entry.owner;
|
|
901
|
+
const members = membersByClass.get(ownerFqn);
|
|
902
|
+
if (entry.targetKind === "class") {
|
|
903
|
+
if (!members) {
|
|
904
|
+
validatedEntries.push({
|
|
905
|
+
...entry,
|
|
906
|
+
valid: false,
|
|
907
|
+
issue: `Class "${ownerFqn}" not found in runtime jar.`,
|
|
908
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
909
|
+
});
|
|
910
|
+
invalidCount++;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
const runtimeAccess = accessLevelFromFlags(members.classAccessFlags);
|
|
914
|
+
validatedEntries.push({
|
|
915
|
+
...entry,
|
|
916
|
+
valid: true,
|
|
917
|
+
...(options?.includeRuntimeEvidence
|
|
918
|
+
? {
|
|
919
|
+
resolvedInRuntime: true,
|
|
920
|
+
...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {})
|
|
921
|
+
}
|
|
922
|
+
: {})
|
|
923
|
+
});
|
|
924
|
+
validCount++;
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
if (!members) {
|
|
928
|
+
validatedEntries.push({
|
|
929
|
+
...entry,
|
|
930
|
+
valid: false,
|
|
931
|
+
issue: `Owner class "${ownerFqn}" not found in runtime jar.`,
|
|
932
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
933
|
+
});
|
|
934
|
+
invalidCount++;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (entry.targetKind === "field") {
|
|
938
|
+
const fieldNames = allFieldNames(members);
|
|
939
|
+
const matchedField = members.fields.find((member) => member.name === entry.name);
|
|
940
|
+
if (!matchedField) {
|
|
941
|
+
const suggestions = entry.name ? suggestSimilar(entry.name, fieldNames) : [];
|
|
942
|
+
validatedEntries.push({
|
|
943
|
+
...entry,
|
|
944
|
+
valid: false,
|
|
945
|
+
issue: `Field "${entry.name}" not found in class "${ownerFqn}".`,
|
|
946
|
+
...(suggestions.length > 0 ? { suggestions } : {}),
|
|
947
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
948
|
+
});
|
|
949
|
+
invalidCount++;
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const runtimeAccess = accessLevelFromFlags(matchedField.accessFlags);
|
|
953
|
+
validatedEntries.push({
|
|
954
|
+
...entry,
|
|
955
|
+
valid: true,
|
|
956
|
+
...(options?.includeRuntimeEvidence
|
|
957
|
+
? {
|
|
958
|
+
resolvedInRuntime: true,
|
|
959
|
+
...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {}),
|
|
960
|
+
...(matchedField.jvmDescriptor ? { resolvedRuntimeJvmDescriptor: matchedField.jvmDescriptor } : {}),
|
|
961
|
+
...(matchedField.javaSignature ? { resolvedRuntimeJavaSignature: matchedField.javaSignature } : {})
|
|
962
|
+
}
|
|
963
|
+
: {})
|
|
964
|
+
});
|
|
965
|
+
validCount++;
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const methodNames = allMethodNames(members);
|
|
969
|
+
const matchedMethod = members.methods.find((member) => member.name === entry.name && member.jvmDescriptor === entry.descriptor) ?? members.constructors.find((member) => member.name === entry.name && member.jvmDescriptor === entry.descriptor);
|
|
970
|
+
if (!matchedMethod) {
|
|
971
|
+
const suggestions = entry.name ? suggestSimilar(entry.name, methodNames) : [];
|
|
972
|
+
validatedEntries.push({
|
|
973
|
+
...entry,
|
|
974
|
+
valid: false,
|
|
975
|
+
issue: `Method "${entry.name}" not found in class "${ownerFqn}".`,
|
|
976
|
+
...(suggestions.length > 0 ? { suggestions } : {}),
|
|
977
|
+
...(options?.includeRuntimeEvidence ? { resolvedInRuntime: false } : {})
|
|
978
|
+
});
|
|
979
|
+
invalidCount++;
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const runtimeAccess = accessLevelFromFlags(matchedMethod.accessFlags);
|
|
983
|
+
validatedEntries.push({
|
|
984
|
+
...entry,
|
|
985
|
+
valid: true,
|
|
986
|
+
...(options?.includeRuntimeEvidence
|
|
987
|
+
? {
|
|
988
|
+
resolvedInRuntime: true,
|
|
989
|
+
...(runtimeAccess ? { resolvedRuntimeAccess: runtimeAccess } : {}),
|
|
990
|
+
...(matchedMethod.jvmDescriptor ? { resolvedRuntimeJvmDescriptor: matchedMethod.jvmDescriptor } : {}),
|
|
991
|
+
...(matchedMethod.javaSignature ? { resolvedRuntimeJavaSignature: matchedMethod.javaSignature } : {})
|
|
992
|
+
}
|
|
993
|
+
: {})
|
|
994
|
+
});
|
|
995
|
+
validCount++;
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
valid: invalidCount === 0,
|
|
999
|
+
entries: validatedEntries,
|
|
1000
|
+
summary: {
|
|
1001
|
+
total: parsed.entries.length,
|
|
1002
|
+
valid: validCount,
|
|
1003
|
+
invalid: invalidCount
|
|
1004
|
+
},
|
|
1005
|
+
warnings
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
822
1008
|
//# sourceMappingURL=mixin-validator.js.map
|
package/dist/mod-analyzer.d.ts
CHANGED
package/dist/mod-analyzer.js
CHANGED
|
@@ -72,7 +72,8 @@ const forgeModsTomlSchema = z
|
|
|
72
72
|
.passthrough())
|
|
73
73
|
.optional(),
|
|
74
74
|
dependencies: z.record(z.array(z.unknown())).optional(),
|
|
75
|
-
mixins: z.array(z.object({ config: z.string() }).passthrough()).optional()
|
|
75
|
+
mixins: z.array(z.object({ config: z.string() }).passthrough()).optional(),
|
|
76
|
+
accessTransformers: z.array(z.object({ file: z.string().optional() }).passthrough()).optional()
|
|
76
77
|
})
|
|
77
78
|
.passthrough();
|
|
78
79
|
const legacyForgeSchema = z.array(z
|
|
@@ -214,6 +215,9 @@ function parseForgeMod(content, entries) {
|
|
|
214
215
|
}
|
|
215
216
|
// Mixin configs
|
|
216
217
|
const mixinConfigs = toml.mixins?.map((m) => m.config);
|
|
218
|
+
const accessTransformers = toml.accessTransformers
|
|
219
|
+
?.map((entry) => entry.file)
|
|
220
|
+
.filter((file) => typeof file === "string" && file.trim().length > 0);
|
|
217
221
|
return {
|
|
218
222
|
detectedLoader,
|
|
219
223
|
modId: firstMod?.modId,
|
|
@@ -221,6 +225,7 @@ function parseForgeMod(content, entries) {
|
|
|
221
225
|
modVersion: firstMod?.version,
|
|
222
226
|
description: firstMod?.description,
|
|
223
227
|
mixinConfigs: mixinConfigs && mixinConfigs.length > 0 ? mixinConfigs : undefined,
|
|
228
|
+
accessTransformers: accessTransformers && accessTransformers.length > 0 ? accessTransformers : undefined,
|
|
224
229
|
dependencies: dependencies.length > 0 ? dependencies : undefined
|
|
225
230
|
};
|
|
226
231
|
}
|
|
@@ -288,6 +293,9 @@ export async function analyzeModJar(jarPath, options) {
|
|
|
288
293
|
// Detect loader and parse metadata
|
|
289
294
|
let loader = "unknown";
|
|
290
295
|
let metadata = {};
|
|
296
|
+
const packagedAccessTransformers = [...new Set(entries.filter((entry) => /(^|\/)META-INF\/accesstransformer\.cfg$/i.test(entry) ||
|
|
297
|
+
/(^|\/)[^/]+_at\.cfg$/i.test(entry) ||
|
|
298
|
+
/(^|\/)accesstransformer[^/]*\.cfg$/i.test(entry)))].sort((left, right) => left.localeCompare(right));
|
|
291
299
|
if (entries.includes("fabric.mod.json")) {
|
|
292
300
|
loader = "fabric";
|
|
293
301
|
try {
|
|
@@ -346,6 +354,14 @@ export async function analyzeModJar(jarPath, options) {
|
|
|
346
354
|
loader,
|
|
347
355
|
jarKind,
|
|
348
356
|
...metadata,
|
|
357
|
+
...(packagedAccessTransformers.length > 0 || metadata.accessTransformers
|
|
358
|
+
? {
|
|
359
|
+
accessTransformers: [...new Set([
|
|
360
|
+
...(metadata.accessTransformers ?? []),
|
|
361
|
+
...packagedAccessTransformers
|
|
362
|
+
])]
|
|
363
|
+
}
|
|
364
|
+
: {}),
|
|
349
365
|
classCount,
|
|
350
366
|
...(classes !== undefined ? { classes } : {})
|
|
351
367
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
5
5
|
import { log } from "./logger.js";
|
|
@@ -41,7 +41,7 @@ export class ModDecompileService {
|
|
|
41
41
|
const targetFile = classNameToFilePath(input.className);
|
|
42
42
|
const matched = files.find((f) => f === targetFile || f.endsWith(`/${targetFile}`) || f === input.className);
|
|
43
43
|
if (matched) {
|
|
44
|
-
const content =
|
|
44
|
+
const content = await readFile(join(outputDir, matched), "utf8");
|
|
45
45
|
sourceResult = {
|
|
46
46
|
className: filePathToClassName(matched),
|
|
47
47
|
content,
|
|
@@ -106,7 +106,7 @@ export class ModDecompileService {
|
|
|
106
106
|
details: { className, jarPath, availableCount: files.length }
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
|
-
const fullContent =
|
|
109
|
+
const fullContent = await readFile(join(outputDir, matched), "utf8");
|
|
110
110
|
const totalLines = fullContent.split("\n").length;
|
|
111
111
|
let content = fullContent;
|
|
112
112
|
let truncated;
|
|
@@ -131,7 +131,7 @@ export class ModDecompileService {
|
|
|
131
131
|
const outPath = isAbsolute(input.outputFile)
|
|
132
132
|
? input.outputFile
|
|
133
133
|
: resolvePath(input.outputFile);
|
|
134
|
-
|
|
134
|
+
await writeFile(outPath, content, "utf8");
|
|
135
135
|
outputFilePath = outPath;
|
|
136
136
|
content = `[Written to ${outPath}]`;
|
|
137
137
|
}
|
|
@@ -8,7 +8,7 @@ import { resolveTinyMappingFile } from "./mapping-service.js";
|
|
|
8
8
|
import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
|
|
9
9
|
import { analyzeModJar } from "./mod-analyzer.js";
|
|
10
10
|
import { normalizePathForHost } from "./path-converter.js";
|
|
11
|
-
import {
|
|
11
|
+
import { detectFabricLikeInputNamespace } from "./source-jar-reader.js";
|
|
12
12
|
import { remapJar } from "./tiny-remapper-service.js";
|
|
13
13
|
import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
|
|
14
14
|
function normalizeTargetNamespace(target) {
|
|
@@ -36,59 +36,6 @@ function extractMinecraftVersion(dependencies) {
|
|
|
36
36
|
const match = mcDep.versionRange.match(/(\d+\.\d+(?:\.\d+)?)/);
|
|
37
37
|
return match?.[1];
|
|
38
38
|
}
|
|
39
|
-
function countMatches(input, pattern) {
|
|
40
|
-
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
41
|
-
const globalPattern = new RegExp(pattern.source, flags);
|
|
42
|
-
let count = 0;
|
|
43
|
-
while (globalPattern.exec(input)) {
|
|
44
|
-
count += 1;
|
|
45
|
-
}
|
|
46
|
-
return count;
|
|
47
|
-
}
|
|
48
|
-
async function detectFabricLikeInputNamespace(inputJar) {
|
|
49
|
-
const warnings = [];
|
|
50
|
-
const classEntries = (await listJarEntries(inputJar))
|
|
51
|
-
.filter((entry) => entry.endsWith(".class"))
|
|
52
|
-
.slice(0, 24);
|
|
53
|
-
if (classEntries.length === 0) {
|
|
54
|
-
warnings.push("Could not inspect class entries to detect input mapping; assuming intermediary.");
|
|
55
|
-
return {
|
|
56
|
-
fromNamespace: "intermediary",
|
|
57
|
-
warnings
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
let mojangScore = 0;
|
|
61
|
-
let intermediaryScore = 0;
|
|
62
|
-
for (const entry of classEntries) {
|
|
63
|
-
let text = "";
|
|
64
|
-
try {
|
|
65
|
-
text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
|
|
71
|
-
intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
|
|
72
|
-
intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
|
|
73
|
-
}
|
|
74
|
-
if (mojangScore > intermediaryScore && mojangScore > 0) {
|
|
75
|
-
return {
|
|
76
|
-
fromNamespace: "mojang",
|
|
77
|
-
warnings
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
if (intermediaryScore > mojangScore && intermediaryScore > 0) {
|
|
81
|
-
return {
|
|
82
|
-
fromNamespace: "intermediary",
|
|
83
|
-
warnings
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
warnings.push("Could not confidently detect whether the input jar uses intermediary or mojang names; assuming intermediary.");
|
|
87
|
-
return {
|
|
88
|
-
fromNamespace: "intermediary",
|
|
89
|
-
warnings
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
39
|
async function detectInputNamespaceForLoader(inputJar, loader) {
|
|
93
40
|
if (loader === "fabric" || loader === "quilt") {
|
|
94
41
|
return detectFabricLikeInputNamespace(inputJar);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { mapWithConcurrencyLimit } from "./concurrency.js";
|
|
3
4
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
4
5
|
import { log } from "./logger.js";
|
|
5
6
|
import { validateAndNormalizeJarPath } from "./path-resolver.js";
|
|
@@ -9,6 +10,7 @@ const DEFAULT_LIMIT = 50;
|
|
|
9
10
|
const MAX_LIMIT = 200;
|
|
10
11
|
const MAX_QUERY_LENGTH = 200;
|
|
11
12
|
const CONTEXT_LINES = 1;
|
|
13
|
+
const DECOMPILED_JAVA_READ_CONCURRENCY = 8;
|
|
12
14
|
const METHOD_PATTERN = /^\s*(public|private|protected)\s+.*\(/;
|
|
13
15
|
const FIELD_PATTERN = /^\s*(public|private|protected)\s+(?:static\s+)?(?:final\s+)?[\w<>,\[\]?]+\s+\w+\s*[;=]/;
|
|
14
16
|
function buildRegex(query) {
|
|
@@ -41,6 +43,9 @@ function extractContext(lines, lineIndex) {
|
|
|
41
43
|
function filePathToClassName(filePath) {
|
|
42
44
|
return filePath.replace(/\.java$/, "").replaceAll("/", ".");
|
|
43
45
|
}
|
|
46
|
+
function cloneRegex(regex) {
|
|
47
|
+
return new RegExp(regex.source, regex.flags);
|
|
48
|
+
}
|
|
44
49
|
export class ModSearchService {
|
|
45
50
|
modDecompileService;
|
|
46
51
|
constructor(modDecompileService) {
|
|
@@ -112,65 +117,40 @@ export class ModSearchService {
|
|
|
112
117
|
const hits = [];
|
|
113
118
|
let totalHits = 0;
|
|
114
119
|
let reachedLimit = false;
|
|
115
|
-
for (
|
|
120
|
+
for (let batchStartIndex = 0; batchStartIndex < classNames.length; batchStartIndex += DECOMPILED_JAVA_READ_CONCURRENCY) {
|
|
116
121
|
if (hits.length >= limit) {
|
|
117
122
|
reachedLimit = true;
|
|
118
123
|
break;
|
|
119
124
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
// If searching only classes, skip content search for this file
|
|
135
|
-
if (searchType === "class")
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
// Content/method/field search: read and scan the file
|
|
140
|
-
if (searchType === "method" || searchType === "field" || searchType === "content" || searchType === "all") {
|
|
141
|
-
let content;
|
|
142
|
-
try {
|
|
143
|
-
content = readFileSync(join(outputDir, filePath), "utf8");
|
|
125
|
+
const batchClassNames = classNames.slice(batchStartIndex, batchStartIndex + DECOMPILED_JAVA_READ_CONCURRENCY);
|
|
126
|
+
const batchHitLimit = Math.max(1, limit - hits.length);
|
|
127
|
+
const fileResults = await mapWithConcurrencyLimit(batchClassNames, DECOMPILED_JAVA_READ_CONCURRENCY, async (className) => this.searchDecompiledClassFile({
|
|
128
|
+
className,
|
|
129
|
+
outputDir,
|
|
130
|
+
searchType,
|
|
131
|
+
regex: cloneRegex(regex),
|
|
132
|
+
maxHits: batchHitLimit
|
|
133
|
+
}));
|
|
134
|
+
for (const fileResult of fileResults) {
|
|
135
|
+
if (hits.length >= limit) {
|
|
136
|
+
reachedLimit = true;
|
|
137
|
+
break;
|
|
144
138
|
}
|
|
145
|
-
|
|
146
|
-
// File might not exist at the expected path, skip
|
|
139
|
+
if (fileResult.hits.length === 0) {
|
|
147
140
|
continue;
|
|
148
141
|
}
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!regex.test(lines[i]))
|
|
157
|
-
continue;
|
|
158
|
-
const lineType = classifyLine(lines[i]);
|
|
159
|
-
// Filter by search type
|
|
160
|
-
if (searchType !== "all" && searchType !== lineType)
|
|
161
|
-
continue;
|
|
162
|
-
totalHits++;
|
|
163
|
-
if (hits.length < limit) {
|
|
164
|
-
hits.push({
|
|
165
|
-
type: lineType,
|
|
166
|
-
name: lineType === "content" ? className : extractSymbolName(lines[i], lineType),
|
|
167
|
-
file: filePath,
|
|
168
|
-
line: i + 1,
|
|
169
|
-
context: extractContext(lines, i)
|
|
170
|
-
});
|
|
171
|
-
}
|
|
142
|
+
const remaining = limit - hits.length;
|
|
143
|
+
const acceptedHits = fileResult.hits.slice(0, remaining);
|
|
144
|
+
hits.push(...acceptedHits);
|
|
145
|
+
totalHits += acceptedHits.length;
|
|
146
|
+
if (hits.length >= limit || fileResult.hits.length > remaining) {
|
|
147
|
+
reachedLimit = true;
|
|
148
|
+
break;
|
|
172
149
|
}
|
|
173
150
|
}
|
|
151
|
+
if (reachedLimit) {
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
174
154
|
}
|
|
175
155
|
log("info", "mod-search.done", {
|
|
176
156
|
jarPath,
|
|
@@ -255,6 +235,59 @@ export class ModSearchService {
|
|
|
255
235
|
warnings
|
|
256
236
|
};
|
|
257
237
|
}
|
|
238
|
+
async searchDecompiledClassFile(input) {
|
|
239
|
+
const hits = [];
|
|
240
|
+
const filePath = input.className.replaceAll(".", "/") + ".java";
|
|
241
|
+
if (input.searchType === "class" || input.searchType === "all") {
|
|
242
|
+
const simpleClassName = input.className.split(".").pop() ?? input.className;
|
|
243
|
+
input.regex.lastIndex = 0;
|
|
244
|
+
if (input.regex.test(simpleClassName)) {
|
|
245
|
+
hits.push({
|
|
246
|
+
type: "class",
|
|
247
|
+
name: input.className,
|
|
248
|
+
file: filePath
|
|
249
|
+
});
|
|
250
|
+
if (input.searchType === "class" || hits.length >= input.maxHits) {
|
|
251
|
+
return { hits };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (input.searchType !== "method" &&
|
|
256
|
+
input.searchType !== "field" &&
|
|
257
|
+
input.searchType !== "content" &&
|
|
258
|
+
input.searchType !== "all") {
|
|
259
|
+
return { hits };
|
|
260
|
+
}
|
|
261
|
+
let content;
|
|
262
|
+
try {
|
|
263
|
+
content = await readFile(join(input.outputDir, filePath), "utf8");
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return { hits };
|
|
267
|
+
}
|
|
268
|
+
const lines = content.split("\n");
|
|
269
|
+
for (let i = 0; i < lines.length; i++) {
|
|
270
|
+
if (hits.length >= input.maxHits) {
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
input.regex.lastIndex = 0;
|
|
274
|
+
if (!input.regex.test(lines[i])) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const lineType = classifyLine(lines[i]);
|
|
278
|
+
if (input.searchType !== "all" && input.searchType !== lineType) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
hits.push({
|
|
282
|
+
type: lineType,
|
|
283
|
+
name: lineType === "content" ? input.className : extractSymbolName(lines[i], lineType),
|
|
284
|
+
file: filePath,
|
|
285
|
+
line: i + 1,
|
|
286
|
+
context: extractContext(lines, i)
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return { hits };
|
|
290
|
+
}
|
|
258
291
|
}
|
|
259
292
|
function extractSymbolName(line, type) {
|
|
260
293
|
const trimmed = line.trim();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact-mode response utilities.
|
|
3
|
+
*
|
|
4
|
+
* Applied at the public boundary (runTool) only — internal service types are never modified.
|
|
5
|
+
*/
|
|
6
|
+
/** Tools that accept the `compact` parameter. */
|
|
7
|
+
export declare const COMPACT_ENABLED_TOOL_NAMES: Set<string>;
|
|
8
|
+
/** Mapping-oriented tools that get additional field projection via compactMappingResponse. */
|
|
9
|
+
export declare const COMPACT_MAPPING_TOOL_NAMES: Set<string>;
|
|
10
|
+
/**
|
|
11
|
+
* Double-gated compact check: tool must be in the allowlist AND parsedInput.compact must be true.
|
|
12
|
+
* Prevents activation on passthrough schemas where Zod doesn't strip unknown keys.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isCompactEnabled(tool: string, parsedInput: unknown): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Shallow-strip empty values from a response object.
|
|
17
|
+
* Only operates on the top level — nested structures are preserved as-is.
|
|
18
|
+
* Non-plain objects (Date, Map, class instances) are never treated as empty.
|
|
19
|
+
*/
|
|
20
|
+
export declare function compactResponse(obj: Record<string, unknown>): Record<string, unknown>;
|
|
21
|
+
/** resolve-artifact compact: omit debug/diagnostic fields. */
|
|
22
|
+
export declare function compactArtifactResponse(obj: Record<string, unknown>): Record<string, unknown>;
|
|
23
|
+
/**
|
|
24
|
+
* Mapping tool compact: omit candidates only when provably redundant.
|
|
25
|
+
*
|
|
26
|
+
* Candidates are omitted when ALL of:
|
|
27
|
+
* 1. resolved === true
|
|
28
|
+
* 2. resolvedSymbol exists
|
|
29
|
+
* 3. candidates is an array of length 1
|
|
30
|
+
* 4. candidateCount === 1
|
|
31
|
+
* 5. candidatesTruncated is falsy
|
|
32
|
+
* 6. candidates[0].matchKind === "exact"
|
|
33
|
+
* 7. candidates[0].confidence is undefined or 1
|
|
34
|
+
*/
|
|
35
|
+
export declare function compactMappingResponse(obj: Record<string, unknown>): Record<string, unknown>;
|