Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
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.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;

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 BoundLabel(Lang.getBinding("service.analysis.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<String> 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 BoundLabel(Lang.getBinding("service.analysis.is-fabric-mod"));
consumer.appendSummary(title);

if (!finalMcVersion.isEmpty()) {
consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + " " + 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")));
}
});

}

// 2. Try to find Forge mod information
FileInfo forgeFileInfo = resource.getFileBundle().get("mcmod.info");
if (forgeFileInfo != null) {
foundAny = true;
String mcVersion = "";
List<String> 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 BoundLabel(Lang.getBinding("service.analysis.is-forge-mod"));
consumer.appendSummary(title);

if (!finalMcVersion.isEmpty()) {
consumer.appendSummary(new Label(Lang.getBinding("service.analysis.minecraft-version").get() + " " + finalMcVersion));
} else {
consumer.appendSummary(new BoundLabel(Lang.getBinding("service.analysis.minecraft-version-unknown")));
}

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 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(WorkspaceResource resource, String className) {
String classPath = className.replace('.', '/');
return resource.getJvmClassBundle().get(classPath);
}
}
6 changes: 6 additions & 0 deletions recaf-ui/src/main/resources/translations/en_US.lang
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions recaf-ui/src/main/resources/translations/zh_CN.lang
Original file line number Diff line number Diff line change
Expand Up @@ -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=方法名称
Expand Down