From 353bf60b13c8d425cffb0a4417cda5881b4b0b0d Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Tue, 2 Sep 2025 09:17:20 +0800 Subject: [PATCH 1/3] 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 e146108cd2848df1ad11fbe8a34e423e63d0474e Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Tue, 2 Sep 2025 09:38:28 +0800 Subject: [PATCH 2/3] 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 1d524c21d9216d10250a7a2850848f24591cb959 Mon Sep 17 00:00:00 2001 From: Canrad <1517807724@qq.com> Date: Tue, 2 Sep 2025 09:45:58 +0800 Subject: [PATCH 3/3] 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=方法名称