From 9ac876ae0468cd10d4468f59924970b172dfb505 Mon Sep 17 00:00:00 2001 From: TheWakz Date: Fri, 19 Jun 2026 09:59:45 +0200 Subject: [PATCH 1/5] fix(analysis): fix display name typo in Forge entry point kind --- .../coley/recaf/services/analysis/entry/EntryPointKind.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java index 9b2ad3c06..afd4147e5 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java @@ -16,5 +16,5 @@ public record EntryPointKind(@Nonnull String id, @Nonnull String displayName) { public static final EntryPointKind JVM_MAIN_METHOD = new EntryPointKind("jvm-main-method", "Java main(String[])"); public static final EntryPointKind ANDROID_ACTIVITY = new EntryPointKind("android-activity", "Android activity"); public static final EntryPointKind MC_FABRIC_MOD_INIT = new EntryPointKind("mc.fabric", "Fabric mod initializer"); - public static final EntryPointKind MC_FORGE_MOD_INIT = new EntryPointKind("mc.forge", "Fabric mod initializer"); + public static final EntryPointKind MC_FORGE_MOD_INIT = new EntryPointKind("mc.forge", "Forge mod initializer"); } From 8b32751ad803d80ac3b58f31b8521f46cbe0ddfa Mon Sep 17 00:00:00 2001 From: TheWakz Date: Fri, 19 Jun 2026 10:00:15 +0200 Subject: [PATCH 2/5] feat(analysis): add Bukkit and Velocity entry point kinds --- .../coley/recaf/services/analysis/entry/EntryPointKind.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java index afd4147e5..e78b5af20 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java @@ -17,4 +17,6 @@ public record EntryPointKind(@Nonnull String id, @Nonnull String displayName) { public static final EntryPointKind ANDROID_ACTIVITY = new EntryPointKind("android-activity", "Android activity"); public static final EntryPointKind MC_FABRIC_MOD_INIT = new EntryPointKind("mc.fabric", "Fabric mod initializer"); public static final EntryPointKind MC_FORGE_MOD_INIT = new EntryPointKind("mc.forge", "Forge mod initializer"); + public static final EntryPointKind MC_BUKKIT_PLUGIN_INIT = new EntryPointKind("mc.bukkit", "Bukkit plugin initializer"); + public static final EntryPointKind MC_VELOCITY_PLUGIN_INIT = new EntryPointKind("mc.velocity", "Velocity plugin initializer"); } From 4aa3d5019945392ea6551207850229e30a800d9e Mon Sep 17 00:00:00 2001 From: TheWakz Date: Fri, 19 Jun 2026 10:00:46 +0200 Subject: [PATCH 3/5] feat(analysis): implement Bukkit and Velocity entry point discovery --- .../BukkitPluginEntryPointDiscovery.java | 74 ++++++++++++++++ .../VelocityPluginEntryPointDiscovery.java | 86 +++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/BukkitPluginEntryPointDiscovery.java create mode 100644 recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/VelocityPluginEntryPointDiscovery.java diff --git a/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/BukkitPluginEntryPointDiscovery.java b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/BukkitPluginEntryPointDiscovery.java new file mode 100644 index 000000000..459b0cce5 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/BukkitPluginEntryPointDiscovery.java @@ -0,0 +1,74 @@ +package software.coley.recaf.services.analysis.entry; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNodes; +import software.coley.recaf.services.inheritance.InheritanceGraph; +import software.coley.recaf.services.inheritance.InheritanceGraphService; +import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.resource.WorkspaceResource; + +import java.util.ArrayList; +import java.util.List; + +/** + * Discovery process for locating entry points in Bukkit plugins. + * + * @author Matt Coley + * @author TheWakz + * @see Bukkit plugin entry points + */ +@ApplicationScoped +public class BukkitPluginEntryPointDiscovery implements EntryPointDiscovery { + private final InheritanceGraphService inheritanceGraphService; + + @Inject + public BukkitPluginEntryPointDiscovery(@Nonnull InheritanceGraphService inheritanceGraphService) { + this.inheritanceGraphService = inheritanceGraphService; + } + + @Nonnull + @Override + public EntryPointKind kind() { + return EntryPointKind.MC_BUKKIT_PLUGIN_INIT; + } + + @Nonnull + @Override + public List findEntryPoints(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource) { + InheritanceGraph graph = inheritanceGraphService.getOrCreateInheritanceGraph(workspace); + List entries = new ArrayList<>(); + resource.jvmAllClassBundleStream().forEach(bundle -> bundle.forEach(cls -> { + String className = cls.getName(); + ClassPathNode classPath = null; + Boolean isPlugin = null; + for (MethodMember method : cls.getMethods()) { + // Must be a plugin enable or load method name. + String methodName = method.getName(); + if (!methodName.equals("onEnable") && !methodName.equals("onLoad")) + continue; + + // Lazily check if this class is a plugin subtype. + if (isPlugin == null) + isPlugin = isPlugin(graph, className); + + // Skip methods if the class is not a valid plugin. + if (!isPlugin) + continue; + + // Add the entry point. + if (classPath == null) + classPath = PathNodes.classPath(workspace, resource, bundle, cls); + entries.add(new EntryPoint(kind(), classPath, classPath.child(method))); + } + })); + return entries; + } + + private static boolean isPlugin(@Nonnull InheritanceGraph graph, @Nonnull String className) { + return graph.isAssignableFrom("org/bukkit/plugin/java/JavaPlugin", className); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/VelocityPluginEntryPointDiscovery.java b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/VelocityPluginEntryPointDiscovery.java new file mode 100644 index 000000000..0b4758b0c --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/VelocityPluginEntryPointDiscovery.java @@ -0,0 +1,86 @@ +package software.coley.recaf.services.analysis.entry; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import software.coley.collections.Sets; +import software.coley.recaf.info.annotation.AnnotationInfo; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNodes; +import software.coley.recaf.services.inheritance.InheritanceGraph; +import software.coley.recaf.services.inheritance.InheritanceGraphService; +import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.resource.WorkspaceResource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Discovery process for locating entry points in Velocity plugins. + * + * @author Matt Coley + * @author TheWakz + * @see Velocity plugin entry points + */ +@ApplicationScoped +public class VelocityPluginEntryPointDiscovery implements EntryPointDiscovery { + private final InheritanceGraphService inheritanceGraphService; + + @Inject + public VelocityPluginEntryPointDiscovery(@Nonnull InheritanceGraphService inheritanceGraphService) { + this.inheritanceGraphService = inheritanceGraphService; + } + + @Nonnull + @Override + public EntryPointKind kind() { + return EntryPointKind.MC_VELOCITY_PLUGIN_INIT; + } + + @Nonnull + @Override + public List findEntryPoints(@Nonnull Workspace workspace, @Nonnull WorkspaceResource resource) { + InheritanceGraph graph = inheritanceGraphService.getOrCreateInheritanceGraph(workspace); + List entries = new ArrayList<>(); + resource.jvmAllClassBundleStream().forEach(bundle -> bundle.forEach(cls -> { + ClassPathNode classPath = null; + EntryPoint classEntry = null; + + // Check for @Plugin annotations + Set annotations = Sets.combine( + cls.getAnnotations().stream().map(AnnotationInfo::getDescriptor).collect(Collectors.toSet()), + cls.getTypeAnnotations().stream().map(AnnotationInfo::getDescriptor).collect(Collectors.toSet()) + ); + for (String annotation : annotations) { + if ("Lcom/velocitypowered/api/plugin/Plugin;".equals(annotation)) { + classPath = PathNodes.classPath(workspace, resource, bundle, cls); + classEntry = new EntryPoint(kind(), classPath, null); + break; + } + } + + // Check for initialization event receivers + boolean foundEventReceiver = false; + for (MethodMember method : cls.getMethods()) { + String desc = method.getDescriptor(); + if ("(Lcom/velocitypowered/api/event/proxy/ProxyInitializeEvent;)V".equals(desc) + || "(Lcom/velocitypowered/api/event/proxy/ProxyReloadEvent;)V".equals(desc) + ) { + if (classPath == null) + classPath = PathNodes.classPath(workspace, resource, bundle, cls); + entries.add(new EntryPoint(kind(), classPath, classPath.child(method))); + foundEventReceiver = true; + } + } + + // Only add the class as an entry point if it wasn't already added as an event receiver. + // The event receiver is more specific and the containing class shares the same path. + if (!foundEventReceiver && classEntry != null) + entries.add(classEntry); + })); + return entries; + } +} From 1fd8a4c28f0e1fe68227d03bc70c4c17811c51c3 Mon Sep 17 00:00:00 2001 From: TheWakz Date: Fri, 19 Jun 2026 10:01:21 +0200 Subject: [PATCH 4/5] feat(i18n): add en_US keys for Bukkit and Velocity entry points --- recaf-ui/src/main/resources/translations/en_US.lang | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index 39499b08b..3911f8ed5 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -811,6 +811,8 @@ service.analysis.entry-points.kind.jvm-main-method=Java main(String[]) service.analysis.entry-points.kind.android-activity=Android activity service.analysis.entry-points.kind.mc.fabric=Fabric mod initializer service.analysis.entry-points.kind.mc.forge=Forge mod initializer +service.analysis.entry-points.kind.mc.bukkit=Bukkit plugin initializer +service.analysis.entry-points.kind.mc.velocity=Velocity plugin initializer service.analysis.entry-points.none=No entries found service.analysis.hashing=Hashing service.analysis.hashing.type=Type From b70c576d7ea435fc2a8220780d4eab68e6f48db2 Mon Sep 17 00:00:00 2001 From: TheWakz Date: Fri, 19 Jun 2026 11:54:52 +0200 Subject: [PATCH 5/5] test(analysis): add entry point discovery tests for Bukkit, Velocity, Forge and Fabric --- .../analysis/EntryAnalysisServiceTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/recaf-core/src/test/java/software/coley/recaf/services/analysis/EntryAnalysisServiceTest.java b/recaf-core/src/test/java/software/coley/recaf/services/analysis/EntryAnalysisServiceTest.java index 59703e613..ed9a02afc 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/analysis/EntryAnalysisServiceTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/analysis/EntryAnalysisServiceTest.java @@ -93,6 +93,84 @@ void findsAndroidActivityEntryPoint() { assertEquals(entry.classPath(), entry.targetPath()); } + @Test + void findsFabricModEntryPoint() { + JvmClassInfo fabricClass = createClass("test/TestFabricMod", node -> { + node.interfaces.add("net/fabricmc/api/ModInitializer"); + + MethodNode initMethod = new MethodNode(Opcodes.ACC_PUBLIC, "onInitialize", "()V", null, null); + initMethod.visitCode(); + initMethod.visitInsn(Opcodes.RETURN); + initMethod.visitEnd(); + node.methods.add(initMethod); + }); + + Workspace workspace = fromBundle(fromClasses(fabricClass)); + List results = service.findEntryPoints(workspace, workspace.getPrimaryResource()); + + assertEquals(1, results.size(), "Should find exactly one entry point"); + assertEquals(EntryPointKind.MC_FABRIC_MOD_INIT, results.getFirst().kind(), "Should be Fabric entry point"); + } + + @Test + void findsForgeModEntryPoint() { + JvmClassInfo forgeModClass = createClass("test/TestForgeMod", node -> { + node.visitAnnotation("Lnet/minecraftforge/fml/common/Mod;", true).visitEnd(); + + MethodNode setupMethod = new MethodNode(Opcodes.ACC_PUBLIC, "setup", + "(Lnet/minecraftforge/fml/event/lifecycle/FMLCommonSetupEvent;)V", null, null); + setupMethod.visitCode(); + setupMethod.visitInsn(Opcodes.RETURN); + setupMethod.visitEnd(); + node.methods.add(setupMethod); + }); + + Workspace workspace = fromBundle(fromClasses(forgeModClass)); + List results = service.findEntryPoints(workspace, workspace.getPrimaryResource()); + + assertEquals(1, results.size(), "Should find exactly one entry point"); + assertEquals(EntryPointKind.MC_FORGE_MOD_INIT, results.getFirst().kind(), "Should be Forge entry point"); + } + + @Test + void findsBukkitPluginEntryPoint() { + JvmClassInfo bukkitPluginClass = createClass("test/TestBukkitPlugin", node -> { + node.superName = "org/bukkit/plugin/java/JavaPlugin"; + + MethodNode onEnableMethod = new MethodNode(Opcodes.ACC_PUBLIC, "onEnable", "()V", null, null); + onEnableMethod.visitCode(); + onEnableMethod.visitInsn(Opcodes.RETURN); + onEnableMethod.visitEnd(); + node.methods.add(onEnableMethod); + }); + + Workspace workspace = fromBundle(fromClasses(bukkitPluginClass)); + List results = service.findEntryPoints(workspace, workspace.getPrimaryResource()); + + assertEquals(1, results.size(), "Should find exactly one entry point"); + assertEquals(EntryPointKind.MC_BUKKIT_PLUGIN_INIT, results.getFirst().kind(), "Should be Bukkit entry point"); + } + + @Test + void findsVelocityPluginEntryPoint() { + JvmClassInfo velocityPluginClass = createClass("test/TestVelocityPlugin", node -> { + node.visitAnnotation("Lcom/velocitypowered/api/plugin/Plugin;", true).visitEnd(); + + MethodNode initMethod = new MethodNode(Opcodes.ACC_PUBLIC, "onInit", + "(Lcom/velocitypowered/api/event/proxy/ProxyInitializeEvent;)V", null, null); + initMethod.visitCode(); + initMethod.visitInsn(Opcodes.RETURN); + initMethod.visitEnd(); + node.methods.add(initMethod); + }); + + Workspace workspace = fromBundle(fromClasses(velocityPluginClass)); + List results = service.findEntryPoints(workspace, workspace.getPrimaryResource()); + + assertEquals(1, results.size(), "Should find exactly one entry point"); + assertEquals(EntryPointKind.MC_VELOCITY_PLUGIN_INIT, results.getFirst().kind(), "Should be Velocity entry point"); + } + @Test void supportsRuntimeDiscoveryRegistration() throws IOException { Workspace workspace = fromBundle(fromClasses(HelloWorld.class));