From cb600da5f670fe68f28596acd6676bf01e4ad96b Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Tue, 2 Sep 2025 09:17:20 +0800 Subject: [PATCH 1/6] Add MinecraftModSummarizer for mod info detection --- .../builtin/MinecraftModSummarizer.java | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java new file mode 100644 index 000000000..52238fac8 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java @@ -0,0 +1,223 @@ +package software.coley.recaf.services.info.summary.builtin; + +import atlantafx.base.theme.Styles; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.control.Label; +import software.coley.recaf.info.FileInfo; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.services.cell.icon.IconProviderService; +import software.coley.recaf.services.cell.text.TextProviderService; +import software.coley.recaf.services.info.summary.ResourceSummarizer; +import software.coley.recaf.services.info.summary.SummaryConsumer; +import software.coley.recaf.services.navigation.Actions; +import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.util.threading.Batch; +import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.resource.WorkspaceResource; + +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.Manifest; + +/** + * Summarizer that finds Minecraft mods main mod classes. + * + * @Author Canrad + */ +@ApplicationScoped +public class MinecraftModSummarizer implements ResourceSummarizer { + private final TextProviderService textService; + private final IconProviderService iconService; + private final Actions actions; + + @Inject + public MinecraftModSummarizer(@Nonnull TextProviderService textService, + @Nonnull IconProviderService iconService, + @Nonnull Actions actions) { + this.textService = textService; + this.iconService = iconService; + this.actions = actions; + } + + @Override + public boolean summarize(@Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull SummaryConsumer consumer) { + Batch batch = FxThreadUtil.batch(); + boolean foundAny = false; + + // Add title + batch.add(() -> { + Label title = new Label("Minecraft Mod Info"); + title.getStyleClass().addAll(Styles.TITLE_4); + consumer.appendSummary(title); + }); + + // 1. Try to find Fabric mod information + FileInfo fabricFileInfo = resource.getFileBundle().get("fabric.mod.json"); + if (fabricFileInfo != null) { + foundAny = true; + String mcVersion = ""; + List mainClasses = new ArrayList<>(); + + try { + String jsonText = fabricFileInfo.asTextFile().getText(); + JsonObject json = JsonParser.parseString(jsonText).getAsJsonObject(); + if (json.has("entrypoints")) { + JsonObject entrypoints = json.getAsJsonObject("entrypoints"); + if (entrypoints.has("main")) { + JsonArray mainArray = entrypoints.getAsJsonArray("main"); + for (int i = 0; i < mainArray.size(); i++) { + String mainClass = mainArray.get(i).getAsString(); + mainClasses.add(mainClass); + } + } + } + + if (json.has("depends")) { + JsonObject depends = json.getAsJsonObject("depends"); + if (depends.has("minecraft")) { + mcVersion = depends.get("minecraft").getAsString(); + } + } + } catch (Exception e) { + // Ignore JSON parsing errors + } + + String finalMcVersion = mcVersion; + batch.add(() -> { + Label title = new Label("Mod Loader: Fabric"); + consumer.appendSummary(title); + + if (!finalMcVersion.isEmpty()) { + consumer.appendSummary(new Label("Minecraft Version: " + finalMcVersion)); + } else { + consumer.appendSummary(new Label("Minecraft Version: (not specified)")); + } + + if (mainClasses.isEmpty()) { + consumer.appendSummary(new Label("Fabric: (no main class specified)")); + } else { + consumer.appendSummary(new Label("Entry Point:")); + } + for (String mainClass : mainClasses) { + // Try to find the main class in JVM class bundle + JvmClassInfo classInfo = findClassInResource(resource, mainClass); + if (classInfo != null) { + // Found class, create label with icon + String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeText(); + Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeIcon(); + Label classLabel = new Label(classDisplay, classIcon); + classLabel.setCursor(Cursor.HAND); + classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, + resource.getJvmClassBundle(), classInfo)); + consumer.appendSummary(classLabel); + } + } + }); + + } + + // 2. Try to find Forge mod information + FileInfo forgeFileInfo = resource.getFileBundle().get("mcmod.info"); + if (forgeFileInfo != null) { + foundAny = true; + String mcVersion = ""; + List corePlugins = new ArrayList<>(); + + try { + // Parse mcmod.info to get mcversion + String jsonText = forgeFileInfo.asTextFile().getText(); + JsonArray modArray = JsonParser.parseString(jsonText).getAsJsonArray(); + if (!modArray.isEmpty()) { + JsonObject modInfo = modArray.get(0).getAsJsonObject(); + if (modInfo.has("mcversion")) { + mcVersion = modInfo.get("mcversion").getAsString(); + } + } + } catch (Exception e) { + // Ignore JSON parsing errors + } + + // Parse manifest to get FMLCorePlugin + FileInfo manifestFileInfo = resource.getFileBundle().get("META-INF/MANIFEST.MF"); + if (manifestFileInfo != null) { + try { + String manifest = manifestFileInfo.asTextFile().getText(); + Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes())); + String corePlugin = mf.getMainAttributes().getValue("FMLCorePlugin"); + if (corePlugin != null && !corePlugin.isEmpty()) { + corePlugins.add(corePlugin); + } + } catch (Exception e) { + // Ignore manifest parsing errors + } + } + + String finalMcVersion = mcVersion; + batch.add(() -> { + Label title = new Label("Mod Loader: Forge"); + consumer.appendSummary(title); + + if (!finalMcVersion.isEmpty()) { + consumer.appendSummary(new Label("Minecraft Version: " + finalMcVersion)); + } else { + consumer.appendSummary(new Label("Minecraft Version: (not specified)")); + } + + if (corePlugins.isEmpty()) { + consumer.appendSummary(new Label("Forge: (no core plugin specified)")); + } else { + consumer.appendSummary(new Label("Entry Point:")); + } + + for (String corePlugin : corePlugins) { + // Try to find the core plugin class in JVM class bundle + JvmClassInfo classInfo = findClassInResource(resource, corePlugin); + if (classInfo != null) { + // Found class, create label with icon + String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeText(); + Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeIcon(); + Label classLabel = new Label(classDisplay, classIcon); + classLabel.setCursor(Cursor.HAND); + classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, + resource.getJvmClassBundle(), classInfo)); + consumer.appendSummary(classLabel); + } + } + }); + } + + if (!foundAny) { + batch.add(() -> consumer.appendSummary(new Label("No Minecraft mod entry points found"))); + } + + batch.execute(); + return foundAny; + } + + /** + * Find a class in the resource by its name. + * Converts dot notation to slash notation for lookup. + */ + private JvmClassInfo findClassInResource(WorkspaceResource resource, String className) { + String classPath = className.replace('.', '/'); + return resource.getJvmClassBundle().get(classPath); + } +} \ No newline at end of file From c394b373f4036cfbf4f9b28995fbd74ec758291e Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Tue, 2 Sep 2025 09:38:28 +0800 Subject: [PATCH 2/6] Add localization for Minecraft mod summary UI --- .../builtin/MinecraftModSummarizer.java | 104 +++++++++--------- .../main/resources/translations/en_US.lang | 6 + 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java index 52238fac8..f144a1db4 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java @@ -17,7 +17,9 @@ import software.coley.recaf.services.info.summary.ResourceSummarizer; import software.coley.recaf.services.info.summary.SummaryConsumer; import software.coley.recaf.services.navigation.Actions; +import software.coley.recaf.ui.control.BoundLabel; import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.util.Lang; import software.coley.recaf.util.threading.Batch; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -56,7 +58,7 @@ public boolean summarize(@Nonnull Workspace workspace, // Add title batch.add(() -> { - Label title = new Label("Minecraft Mod Info"); + Label title = new BoundLabel(Lang.getBinding("service.analysis.minecraft-mod-info")); title.getStyleClass().addAll(Styles.TITLE_4); consumer.appendSummary(title); }); @@ -94,37 +96,37 @@ public boolean summarize(@Nonnull Workspace workspace, String finalMcVersion = mcVersion; batch.add(() -> { - Label title = new Label("Mod Loader: Fabric"); + Label title = new BoundLabel(Lang.getBinding("service.analysis.is-fabric-mod")); consumer.appendSummary(title); if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new Label("Minecraft Version: " + finalMcVersion)); + consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + finalMcVersion)); } else { - consumer.appendSummary(new Label("Minecraft Version: (not specified)")); + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); } - if (mainClasses.isEmpty()) { - consumer.appendSummary(new Label("Fabric: (no main class specified)")); - } else { - consumer.appendSummary(new Label("Entry Point:")); - } - for (String mainClass : mainClasses) { - // Try to find the main class in JVM class bundle - JvmClassInfo classInfo = findClassInResource(resource, mainClass); - if (classInfo != null) { - // Found class, create label with icon - String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeText(); - Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeIcon(); - Label classLabel = new Label(classDisplay, classIcon); - classLabel.setCursor(Cursor.HAND); - classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, - resource.getJvmClassBundle(), classInfo)); - consumer.appendSummary(classLabel); + if (!mainClasses.isEmpty()) { + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points"))); + for (String mainClass : mainClasses) { + // Try to find the main class in JVM class bundle + JvmClassInfo classInfo = findClassInResource(resource, mainClass); + if (classInfo != null) { + // Found class, create label with icon + String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeText(); + Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeIcon(); + Label classLabel = new Label(classDisplay, classIcon); + classLabel.setCursor(Cursor.HAND); + classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, + resource.getJvmClassBundle(), classInfo)); + consumer.appendSummary(classLabel); + } } + } else { + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points.none"))); } }); @@ -168,44 +170,44 @@ public boolean summarize(@Nonnull Workspace workspace, String finalMcVersion = mcVersion; batch.add(() -> { - Label title = new Label("Mod Loader: Forge"); + Label title = new BoundLabel(Lang.getBinding("service.analysis.is-forge-mod")); consumer.appendSummary(title); if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new Label("Minecraft Version: " + finalMcVersion)); - } else { - consumer.appendSummary(new Label("Minecraft Version: (not specified)")); - } - - if (corePlugins.isEmpty()) { - consumer.appendSummary(new Label("Forge: (no core plugin specified)")); + consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + finalMcVersion)); } else { - consumer.appendSummary(new Label("Entry Point:")); + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); } - for (String corePlugin : corePlugins) { - // Try to find the core plugin class in JVM class bundle - JvmClassInfo classInfo = findClassInResource(resource, corePlugin); - if (classInfo != null) { - // Found class, create label with icon - String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeText(); - Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeIcon(); - Label classLabel = new Label(classDisplay, classIcon); - classLabel.setCursor(Cursor.HAND); - classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, - resource.getJvmClassBundle(), classInfo)); - consumer.appendSummary(classLabel); + if (!corePlugins.isEmpty()) { + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points"))); + for (String corePlugin : corePlugins) { + // Try to find the core plugin class in JVM class bundle + JvmClassInfo classInfo = findClassInResource(resource, corePlugin); + if (classInfo != null) { + // Found class, create label with icon + String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeText(); + Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeIcon(); + Label classLabel = new Label(classDisplay, classIcon); + classLabel.setCursor(Cursor.HAND); + classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, + resource.getJvmClassBundle(), classInfo)); + consumer.appendSummary(classLabel); + } } + + } else { + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points.none"))); } }); } if (!foundAny) { - batch.add(() -> consumer.appendSummary(new Label("No Minecraft mod entry points found"))); + batch.add(() -> consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.no-minecraft-mod-found")))); } batch.execute(); diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index 3c853be39..6f71a23e6 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -559,6 +559,12 @@ service.analysis.jphantom-generator-config.generate-workspace-phantoms=Generate service.analysis.search-config=Search service.analysis.entry-points=Entry points service.analysis.entry-points.none=No entries found +service.analysis.minecraft-mod-info=Minecraft Mod Info +service.analysis.is-fabric-mod=Mod Loader: Fabric +service.analysis.is-forge-mod=Mod Loader: Forge +service.analysis.minecraft-version=Minecraft Version: +service.analysis.minecraft-version-unknown=Minecraft Version: (not specified) +service.analysis.no-minecraft-mod-found=No Minecraft mod entry points found service.analysis.anti-decompile=Anti-Decompilation service.analysis.anti-decompile.illegal-attr=Illegal attributes service.analysis.anti-decompile.illegal-name=Illegal names From 57c11cd069320ef414e0dab395743a6b7f98db9d Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Tue, 2 Sep 2025 09:45:58 +0800 Subject: [PATCH 3/6] Improve Minecraft version label formatting and add Chinese translations --- .../info/summary/builtin/MinecraftModSummarizer.java | 4 ++-- recaf-ui/src/main/resources/translations/zh_CN.lang | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java index f144a1db4..f2081b45c 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java @@ -100,7 +100,7 @@ public boolean summarize(@Nonnull Workspace workspace, consumer.appendSummary(title); if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + finalMcVersion)); + consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + " " + finalMcVersion)); } else { consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); } @@ -174,7 +174,7 @@ public boolean summarize(@Nonnull Workspace workspace, consumer.appendSummary(title); if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + finalMcVersion)); + consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + " " + finalMcVersion)); } else { consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); } diff --git a/recaf-ui/src/main/resources/translations/zh_CN.lang b/recaf-ui/src/main/resources/translations/zh_CN.lang index d39dc6ee8..5d494b10b 100644 --- a/recaf-ui/src/main/resources/translations/zh_CN.lang +++ b/recaf-ui/src/main/resources/translations/zh_CN.lang @@ -486,6 +486,12 @@ service.analysis.jphantom-generator-config.generate-workspace-phantoms=生成并 service.analysis.search-config=搜索 service.analysis.entry-points=入口点 service.analysis.entry-points.none=未找到入口点 +service.analysis.minecraft-mod-info=Minecraft Mod信息 +service.analysis.is-fabric-mod=Mod加载器: Fabric +service.analysis.is-forge-mod=Mod加载器: Forge +service.analysis.minecraft-version=Minecraft版本: +service.analysis.minecraft-version-unknown=Minecraft版本: 不确定 +service.analysis.no-minecraft-mod-found=没有找到Minecraft mod入口点 service.analysis.anti-decompile=反-反编译 service.analysis.anti-decompile.illegal-attr=非法属性 service.analysis.anti-decompile.illegal-name=方法名称 From bb120a6355593fae3e77c13b9590cac1c3648d92 Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Fri, 5 Sep 2025 10:32:52 +0800 Subject: [PATCH 4/6] Improve Minecraft mod summarizer entrypoint and version parsing --- .../builtin/MinecraftModSummarizer.java | 36 ++++++++++++++++--- .../main/resources/translations/en_US.lang | 2 +- .../main/resources/translations/zh_CN.lang | 2 +- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java index f2081b45c..b9587e472 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java @@ -73,21 +73,49 @@ public boolean summarize(@Nonnull Workspace workspace, try { String jsonText = fabricFileInfo.asTextFile().getText(); JsonObject json = JsonParser.parseString(jsonText).getAsJsonObject(); + // reference: https://fabricmc.net/wiki/documentation:fabricmodjson + // we need consider 'main', 'client', 'server' if (json.has("entrypoints")) { JsonObject entrypoints = json.getAsJsonObject("entrypoints"); - if (entrypoints.has("main")) { + if (entrypoints.has("main") && entrypoints.get("main").isJsonArray()) { JsonArray mainArray = entrypoints.getAsJsonArray("main"); for (int i = 0; i < mainArray.size(); i++) { String mainClass = mainArray.get(i).getAsString(); mainClasses.add(mainClass); } } + else if (entrypoints.has("client") && entrypoints.get("client").isJsonArray()) { + JsonArray clientArray = entrypoints.getAsJsonArray("client"); + for (int i = 0; i < clientArray.size(); i++) { + String mainClass = clientArray.get(i).getAsString(); + mainClasses.add(mainClass); + } + } else if (entrypoints.has("server") && entrypoints.get("server").isJsonArray()) { + JsonArray serverArray = entrypoints.getAsJsonArray("server"); + for (int i = 0; i < serverArray.size(); i++) { + String mainClass = serverArray.get(i).getAsString(); + mainClasses.add(mainClass); + } + } } if (json.has("depends")) { JsonObject depends = json.getAsJsonObject("depends"); if (depends.has("minecraft")) { - mcVersion = depends.get("minecraft").getAsString(); + if(!depends.get("minecraft").isJsonArray()){ + mcVersion = depends.get("minecraft").getAsString(); + } else { + // sometimes the minecraft version is an array + // we connect them with ', ' + JsonArray mcArray = depends.getAsJsonArray("minecraft"); + if(!mcArray.isEmpty()){ + StringBuilder sb = new StringBuilder(mcArray.get(0).getAsString()); + for(int i = 1; i < mcArray.size(); i++) { + sb.append(", ").append(mcArray.get(i).getAsString()); + } + mcVersion = sb.toString(); + } + } } } } catch (Exception e) { @@ -100,7 +128,7 @@ public boolean summarize(@Nonnull Workspace workspace, consumer.appendSummary(title); if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + " " + finalMcVersion)); + consumer.appendSummary(new BoundLabel(Lang.format("service.analysis.minecraft-version", finalMcVersion))); } else { consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); } @@ -174,7 +202,7 @@ public boolean summarize(@Nonnull Workspace workspace, consumer.appendSummary(title); if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + " " + finalMcVersion)); + consumer.appendSummary(new BoundLabel(Lang.format("service.analysis.minecraft-version", finalMcVersion))); } else { consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); } diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index 6f71a23e6..80637c1be 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -562,7 +562,7 @@ service.analysis.entry-points.none=No entries found service.analysis.minecraft-mod-info=Minecraft Mod Info service.analysis.is-fabric-mod=Mod Loader: Fabric service.analysis.is-forge-mod=Mod Loader: Forge -service.analysis.minecraft-version=Minecraft Version: +service.analysis.minecraft-version=Minecraft Version: %s service.analysis.minecraft-version-unknown=Minecraft Version: (not specified) service.analysis.no-minecraft-mod-found=No Minecraft mod entry points found service.analysis.anti-decompile=Anti-Decompilation diff --git a/recaf-ui/src/main/resources/translations/zh_CN.lang b/recaf-ui/src/main/resources/translations/zh_CN.lang index 5d494b10b..d73f63b7d 100644 --- a/recaf-ui/src/main/resources/translations/zh_CN.lang +++ b/recaf-ui/src/main/resources/translations/zh_CN.lang @@ -489,7 +489,7 @@ service.analysis.entry-points.none=未找到入口点 service.analysis.minecraft-mod-info=Minecraft Mod信息 service.analysis.is-fabric-mod=Mod加载器: Fabric service.analysis.is-forge-mod=Mod加载器: Forge -service.analysis.minecraft-version=Minecraft版本: +service.analysis.minecraft-version=Minecraft版本: %s service.analysis.minecraft-version-unknown=Minecraft版本: 不确定 service.analysis.no-minecraft-mod-found=没有找到Minecraft mod入口点 service.analysis.anti-decompile=反-反编译 From 586005ab37e6f8afd8ce8c902e27cf514de40538 Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Fri, 5 Sep 2025 11:09:26 +0800 Subject: [PATCH 5/6] Refactor Minecraft mod summarizer logic --- .../builtin/MinecraftModSummarizer.java | 204 +++++++++--------- 1 file changed, 100 insertions(+), 104 deletions(-) diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java index b9587e472..cd537e97e 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java @@ -39,6 +39,9 @@ public class MinecraftModSummarizer implements ResourceSummarizer { private final TextProviderService textService; private final IconProviderService iconService; private final Actions actions; + private String loaderName = ""; + private String mcVersion = ""; + private final List mainClasses = new ArrayList<>(); @Inject public MinecraftModSummarizer(@Nonnull TextProviderService textService, @@ -54,21 +57,40 @@ public boolean summarize(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource, @Nonnull SummaryConsumer consumer) { Batch batch = FxThreadUtil.batch(); - boolean foundAny = false; - // Add title - batch.add(() -> { - Label title = new BoundLabel(Lang.getBinding("service.analysis.minecraft-mod-info")); - title.getStyleClass().addAll(Styles.TITLE_4); - consumer.appendSummary(title); - }); + loaderName = ""; + mcVersion = ""; + mainClasses.clear(); // 1. Try to find Fabric mod information + boolean foundAny = detectFabricMod(resource); + + // 2. Try to find Forge mod information + foundAny |= detectLowVersionForgeMod(resource); + foundAny |= detectHighVersionForgeMod(resource); + + if (foundAny) { + renderSummary(workspace, resource, consumer, batch); + } else { + batch.add(() -> consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.no-minecraft-mod-found")))); + } + + batch.execute(); + return foundAny; + } + + /** + * Find a class in the resource by its name. + * Converts dot notation to slash notation for lookup. + */ + private JvmClassInfo findClassInResource(@Nonnull WorkspaceResource resource, String className) { + String classPath = className.replace('.', '/'); + return resource.getJvmClassBundle().get(classPath); + } + + private boolean detectFabricMod(@Nonnull WorkspaceResource resource) { FileInfo fabricFileInfo = resource.getFileBundle().get("fabric.mod.json"); if (fabricFileInfo != null) { - foundAny = true; - String mcVersion = ""; - List mainClasses = new ArrayList<>(); try { String jsonText = fabricFileInfo.asTextFile().getText(); @@ -83,8 +105,7 @@ public boolean summarize(@Nonnull Workspace workspace, String mainClass = mainArray.get(i).getAsString(); mainClasses.add(mainClass); } - } - else if (entrypoints.has("client") && entrypoints.get("client").isJsonArray()) { + } else if (entrypoints.has("client") && entrypoints.get("client").isJsonArray()) { JsonArray clientArray = entrypoints.getAsJsonArray("client"); for (int i = 0; i < clientArray.size(); i++) { String mainClass = clientArray.get(i).getAsString(); @@ -102,15 +123,15 @@ else if (entrypoints.has("client") && entrypoints.get("client").isJsonArray()) { if (json.has("depends")) { JsonObject depends = json.getAsJsonObject("depends"); if (depends.has("minecraft")) { - if(!depends.get("minecraft").isJsonArray()){ + if (!depends.get("minecraft").isJsonArray()) { mcVersion = depends.get("minecraft").getAsString(); } else { // sometimes the minecraft version is an array // we connect them with ', ' JsonArray mcArray = depends.getAsJsonArray("minecraft"); - if(!mcArray.isEmpty()){ + if (!mcArray.isEmpty()) { StringBuilder sb = new StringBuilder(mcArray.get(0).getAsString()); - for(int i = 1; i < mcArray.size(); i++) { + for (int i = 1; i < mcArray.size(); i++) { sb.append(", ").append(mcArray.get(i).getAsString()); } mcVersion = sb.toString(); @@ -122,51 +143,17 @@ else if (entrypoints.has("client") && entrypoints.get("client").isJsonArray()) { // Ignore JSON parsing errors } - String finalMcVersion = mcVersion; - batch.add(() -> { - Label title = new BoundLabel(Lang.getBinding("service.analysis.is-fabric-mod")); - consumer.appendSummary(title); - - if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new BoundLabel(Lang.format("service.analysis.minecraft-version", finalMcVersion))); - } else { - consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); - } - - if (!mainClasses.isEmpty()) { - consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points"))); - for (String mainClass : mainClasses) { - // Try to find the main class in JVM class bundle - JvmClassInfo classInfo = findClassInResource(resource, mainClass); - if (classInfo != null) { - // Found class, create label with icon - String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeText(); - Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeIcon(); - Label classLabel = new Label(classDisplay, classIcon); - classLabel.setCursor(Cursor.HAND); - classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, - resource.getJvmClassBundle(), classInfo)); - consumer.appendSummary(classLabel); - } - } - } else { - consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points.none"))); - } - }); + loaderName = Lang.getBinding("service.analysis.is-fabric-mod").get(); + return true; } + return false; + } - // 2. Try to find Forge mod information + private boolean detectLowVersionForgeMod(@Nonnull WorkspaceResource resource) { FileInfo forgeFileInfo = resource.getFileBundle().get("mcmod.info"); if (forgeFileInfo != null) { - foundAny = true; - String mcVersion = ""; - List corePlugins = new ArrayList<>(); - + // reference: https://docs.minecraftforge.net/en/1.13.x/gettingstarted/structuring/ try { // Parse mcmod.info to get mcversion String jsonText = forgeFileInfo.asTextFile().getText(); @@ -189,65 +176,74 @@ else if (entrypoints.has("client") && entrypoints.get("client").isJsonArray()) { Manifest mf = new Manifest(new ByteArrayInputStream(manifest.getBytes())); String corePlugin = mf.getMainAttributes().getValue("FMLCorePlugin"); if (corePlugin != null && !corePlugin.isEmpty()) { - corePlugins.add(corePlugin); + mainClasses.add(corePlugin); } } catch (Exception e) { // Ignore manifest parsing errors } } - String finalMcVersion = mcVersion; - batch.add(() -> { - Label title = new BoundLabel(Lang.getBinding("service.analysis.is-forge-mod")); - consumer.appendSummary(title); - - if (!finalMcVersion.isEmpty()) { - consumer.appendSummary(new BoundLabel(Lang.format("service.analysis.minecraft-version", finalMcVersion))); - } else { - consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); - } + loaderName = Lang.getBinding("service.analysis.is-forge-mod").get(); + return true; + } + return false; + } - if (!corePlugins.isEmpty()) { - consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points"))); - for (String corePlugin : corePlugins) { - // Try to find the core plugin class in JVM class bundle - JvmClassInfo classInfo = findClassInResource(resource, corePlugin); - if (classInfo != null) { - // Found class, create label with icon - String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeText(); - Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, - resource.getJvmClassBundle(), classInfo).makeIcon(); - Label classLabel = new Label(classDisplay, classIcon); - classLabel.setCursor(Cursor.HAND); - classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); - classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, - resource.getJvmClassBundle(), classInfo)); - consumer.appendSummary(classLabel); - } - } + private boolean detectHighVersionForgeMod(@Nonnull WorkspaceResource resource) { + FileInfo forgeFileInfo = resource.getFileBundle().get("META-INF/mods.toml"); + if (forgeFileInfo != null) { + // reference: https://mcforge.readthedocs.io/en/latest/gettingstarted/modfiles/ - } else { - consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points.none"))); - } - }); + return true; } + return false; + } - if (!foundAny) { - batch.add(() -> consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.no-minecraft-mod-found")))); - } + private void renderSummary(@Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull SummaryConsumer consumer, + @Nonnull Batch batch) { + // Add title + batch.add(() -> { + Label title = new BoundLabel(Lang.getBinding("service.analysis.minecraft-mod-info")); + title.getStyleClass().addAll(Styles.TITLE_4); + consumer.appendSummary(title); + }); - batch.execute(); - return foundAny; - } - /** - * Find a class in the resource by its name. - * Converts dot notation to slash notation for lookup. - */ - private JvmClassInfo findClassInResource(WorkspaceResource resource, String className) { - String classPath = className.replace('.', '/'); - return resource.getJvmClassBundle().get(classPath); + batch.add(() -> { + Label title = new Label(loaderName); + consumer.appendSummary(title); + + if (!mcVersion.isEmpty()) { + consumer.appendSummary(new BoundLabel(Lang.format("service.analysis.minecraft-version", mcVersion))); + } else { + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown"))); + } + + if (!mainClasses.isEmpty()) { + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points"))); + for (String mainClass : mainClasses) { + // Try to find the main class in JVM class bundle + JvmClassInfo classInfo = findClassInResource(resource, mainClass); + if (classInfo != null) { + // Found class, create label with icon + String classDisplay = textService.getJvmClassInfoTextProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeText(); + Node classIcon = iconService.getJvmClassInfoIconProvider(workspace, resource, + resource.getJvmClassBundle(), classInfo).makeIcon(); + Label classLabel = new Label(classDisplay, classIcon); + classLabel.setCursor(Cursor.HAND); + classLabel.setOnMouseEntered(e -> classLabel.getStyleClass().add(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseExited(e -> classLabel.getStyleClass().remove(Styles.TEXT_UNDERLINED)); + classLabel.setOnMouseClicked(e -> actions.gotoDeclaration(workspace, resource, + resource.getJvmClassBundle(), classInfo)); + consumer.appendSummary(classLabel); + } + } + } else { + consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.entry-points.none"))); + } + }); } -} \ No newline at end of file +} From cec9a1ce1e6a90355c91d5ddb64e971c4d46373f Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Fri, 5 Sep 2025 12:08:20 +0800 Subject: [PATCH 6/6] Add support for high version forge mod detection --- gradle/libs.versions.toml | 2 + recaf-ui/build.gradle | 1 + .../builtin/MinecraftModSummarizer.java | 56 +++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0abd05c88..df103fe49 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -144,6 +144,8 @@ vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" sourcesolver = { module = "software.coley:source-solver", version.ref = "sourcesolver" } +toml4j = { module = "io.hotmoka:toml4j", version = "0.7.3" } + [bundles] asm = [ "asm-core", diff --git a/recaf-ui/build.gradle b/recaf-ui/build.gradle index 572d81e27..b099febb1 100644 --- a/recaf-ui/build.gradle +++ b/recaf-ui/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation(libs.reactfx) implementation(libs.richtextfx) implementation(libs.treemapfx) + implementation(libs.toml4j) } application { diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java index cd537e97e..428ce122c 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/MinecraftModSummarizer.java @@ -4,6 +4,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.moandjiezana.toml.Toml; import jakarta.annotation.Nonnull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -27,6 +28,9 @@ import java.io.ByteArrayInputStream; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; import java.util.jar.Manifest; /** @@ -192,7 +196,59 @@ private boolean detectLowVersionForgeMod(@Nonnull WorkspaceResource resource) { private boolean detectHighVersionForgeMod(@Nonnull WorkspaceResource resource) { FileInfo forgeFileInfo = resource.getFileBundle().get("META-INF/mods.toml"); if (forgeFileInfo != null) { + loaderName = Lang.getBinding("service.analysis.is-forge-mod").get(); + // reference: https://mcforge.readthedocs.io/en/latest/gettingstarted/modfiles/ + // 1. find modId in the [[mods]] section + String modId = ""; + Toml toml = new Toml(); + try { + toml.read(forgeFileInfo.asTextFile().getText()); + List mods = toml.getList("mods"); + if (mods != null && !mods.isEmpty()) { + Object firstMod = mods.getFirst(); + if (firstMod instanceof Map map) { + if (map.containsKey("modId")) { + modId = map.get("modId").toString(); + } + } + } + } catch (Exception e) { + // Ignore TOML parsing errors + } + + // 2. find versionRange in the [[dependencies.]] section and the modId of this must is "minecraft" + if (!modId.isEmpty()) { + try { + List dependencies = toml.getList("dependencies." + modId); + if (dependencies != null && !dependencies.isEmpty()) { + for (Object dep : dependencies) { + if (dep instanceof Map map) { + if (map.containsKey("modId") && map.get("modId").toString().equals("minecraft")) { + if (map.containsKey("versionRange")) { + mcVersion = map.get("versionRange").toString(); + break; + } + } + } + } + } + } catch (Exception e) { + // Ignore TOML parsing errors + } + } + + // we can not find main class in mods.toml, so we scan all class files for @Mod annotation + resource.jvmClassBundleStream().forEach(bundle -> { + bundle.forEach(cls -> { + Supplier classLookup = () -> Objects.requireNonNullElse(bundle.get(cls.getName()), cls); + classLookup.get().getAnnotations().forEach(annotationInfo -> { + if (annotationInfo.getDescriptor().equals("Lnet/minecraftforge/fml/common/Mod;")) { + mainClasses.add(cls.getName()); + } + }); + }); + }); return true; }