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/EntryPointKind.java b/recaf-core/src/main/java/software/coley/recaf/services/analysis/entry/EntryPointKind.java
index 9b2ad3c06..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
@@ -16,5 +16,7 @@ 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");
+ 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");
}
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;
+ }
+}
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));
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