diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java
index b77233b12..87b969e25 100644
--- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java
+++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java
@@ -1,47 +1,24 @@
package noppes.npcs.janino;
-import noppes.npcs.constants.EnumScriptType;
-import noppes.npcs.janino.annotations.ScriptHook;
-
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
-import java.util.stream.Collectors;
/**
- * Resolves script hook methods for a given interface type using runtime
+ * Resolves script hook methods for compiled script classes using runtime
* argument values. Resolution favors exact matches and then the "closest"
* assignable parameter types (measured by inheritance distance).
*
- * The resolver keeps simple caches for previously resolved signatures and
- * supports mapping enum-based hook names (via the {@code @ScriptHook}
- * annotation) to methods.
- *
- *
Dynamic Hook Support
- * This resolver supports two modes:
- *
- *
Interface-only: Only methods defined in the interface are found (legacy behavior)
- *
Compiled class: Methods on the actual compiled script class are found,
- * allowing users to define custom methods for Forge events, addon hooks, etc.
- *
- * Use {@link #resolveHookMethod(String, Object[], Object)} to search the compiled instance.
+ * Methods are resolved dynamically from the compiled class - no interface
+ * definitions required.
*/
public final class JaninoHookResolver {
private final Class> interfaceType;
- /* Methods explicitly mapped from EnumScriptType via @ScriptHook */
- private final Map enumHookLookup = new HashMap<>();
-
/**
- * Cache for resolved hook methods. Keys are generated by {@link #buildSignatureKey}
- * and represent the unique combination of hook name and runtime argument types.
- * This avoids expensive recursive reflection lookups on every hook call.
- */
- private final Map signatureCache = new HashMap<>();
-
- /**
- * Cache for methods found on compiled classes (keyed by class + signature).
- * Separate from signatureCache to handle recompilation.
+ * Cache for methods found on compiled classes (keyed by signature).
*/
private final Map compiledClassCache = new HashMap<>();
@@ -51,12 +28,13 @@ public final class JaninoHookResolver {
*/
private Class> lastCompiledClass;
- /* Lazily built list of available hooks and a mapping for display names */
- private boolean hookListInitialized;
- /* List of names displayed in GUI hooks list (accounts for overload names) */
- private final List displayHookList = new ArrayList<>();
- /* Display name -> Method map */
- private final Map displayHookMethods = new HashMap<>();
+ // MethodHandle caches
+ private final Map compiledHandleCache = new HashMap<>();
+
+ // Method -> MethodHandle for identity-based deduplication
+ private final Map methodToHandle = new IdentityHashMap<>();
+
+ private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
/**
* Create a resolver for the provided script interface type.
@@ -64,7 +42,6 @@ public final class JaninoHookResolver {
*/
public JaninoHookResolver(Class> interfaceType) {
this.interfaceType = Objects.requireNonNull(interfaceType, "interfaceType");
- initializeEnumHookMethods();
}
/**
@@ -72,114 +49,64 @@ public final class JaninoHookResolver {
* method resolution. Call when scripts or available methods change.
*/
public void clearResolutionCaches() {
- signatureCache.clear();
compiledClassCache.clear();
+ compiledHandleCache.clear();
+ methodToHandle.clear();
lastCompiledClass = null;
}
/**
- * Resolve a hook method by name and runtime argument values (interface-only mode).
- *
- * This first checks any enum-mapped methods (via {@code @ScriptHook}),
- * then a signature-based cache and finally attempts full candidate
- * resolution using {@link #resolveHookMethodInternal}.
- *
- * @param hookName hook method name (or enum function name)
- * @param args runtime arguments to use for overload resolution
- * @return the resolved {@link Method} or {@code null} if none matched
- */
- public Method resolveHookMethod(String hookName, Object[] args) {
- return resolveHookMethod(hookName, args, null);
- }
-
- /**
- * Resolve a hook method by name, searching both the compiled class and interface.
- *
- *
This enables dynamic hook support: users can define methods for any hook
- * (Forge events, addon hooks, etc.) without needing them in the interface definition.
+ * Resolve a hook method by name, searching the compiled class.
*
*
Resolution order:
*
- *
Enum-mapped methods (via {@code @ScriptHook})
- *
Compiled class methods (user-defined, including custom hooks)
- *
Interface methods (base hooks with defaults)
+ *
Compiled class methods (user-defined)
*
Fallback to no-arg versions
*
*
- *
Performance: Method lookups are cached. First call per hook does reflection
- * (~microseconds), subsequent calls use cache (essentially free).
- *
- * @param hookName hook method name (or enum function name)
+ * @param hookName hook method name
* @param args runtime arguments to use for overload resolution
- * @param compiledInstance the compiled script instance (or null for interface-only)
+ * @param compiledInstance the compiled script instance
* @return the resolved {@link Method} or {@code null} if none matched
*/
public Method resolveHookMethod(String hookName, Object[] args, Object compiledInstance) {
- if (hookName == null || hookName.isEmpty())
+ if (hookName == null || hookName.isEmpty() || compiledInstance == null)
return null;
- // 1. Check enum-mapped methods first (fastest path)
- Method method = enumHookLookup.get(hookName);
- if (method != null)
- return method;
-
Object[] safeArgs = args == null ? new Object[0] : args;
String signatureKey = buildSignatureKey(hookName, safeArgs);
- // 2. If we have a compiled instance, search its class first
- if (compiledInstance != null) {
- Class> compiledClass = compiledInstance.getClass();
+ Class> compiledClass = compiledInstance.getClass();
- // Clear cache if compiled class changed (recompilation)
- if (lastCompiledClass != compiledClass) {
- compiledClassCache.clear();
- lastCompiledClass = compiledClass;
- }
-
- // Check compiled class cache
- method = compiledClassCache.get(signatureKey);
- if (method != null)
- return method;
-
- // Search compiled class for matching method
- method = resolveHookMethodInternal(compiledClass, hookName, safeArgs);
- if (method != null) {
- compiledClassCache.put(signatureKey, method);
- return method;
- }
-
- // Try no-arg version on compiled class
- String noArgKey = buildSignatureKey(hookName, new Object[0]);
- method = compiledClassCache.get(noArgKey);
- if (method != null)
- return method;
-
- method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]);
- if (method != null) {
- compiledClassCache.put(noArgKey, method);
- return method;
- }
+ // Clear cache if compiled class changed (recompilation)
+ if (lastCompiledClass != compiledClass) {
+ compiledClassCache.clear();
+ compiledHandleCache.clear();
+ methodToHandle.clear();
+ lastCompiledClass = compiledClass;
}
- // 3. Fall back to interface-only search
- method = signatureCache.get(signatureKey);
+ // Check compiled class cache
+ Method method = compiledClassCache.get(signatureKey);
if (method != null)
return method;
- method = resolveHookMethodInternal(interfaceType, hookName, safeArgs);
+ // Search compiled class for matching method
+ method = resolveHookMethodInternal(compiledClass, hookName, safeArgs);
if (method != null) {
- signatureCache.put(signatureKey, method);
+ compiledClassCache.put(signatureKey, method);
return method;
}
- String noArgSignatureKey = buildSignatureKey(hookName, new Object[0]);
- method = signatureCache.get(noArgSignatureKey);
+ // Try no-arg version on compiled class
+ String noArgKey = buildSignatureKey(hookName, new Object[0]);
+ method = compiledClassCache.get(noArgKey);
if (method != null)
return method;
- method = resolveHookMethodInternal(interfaceType, hookName, new Object[0]);
+ method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]);
if (method != null) {
- signatureCache.put(noArgSignatureKey, method);
+ compiledClassCache.put(noArgKey, method);
return method;
}
@@ -187,83 +114,55 @@ public final class JaninoHookResolver {
}
/**
- * Returns an immutable copy of the available hook display names.
+ * Resolve a MethodHandle for the given hook. Faster than Method.invoke().
+ * Uses existing Method resolution, then converts to cached MethodHandle.
*/
- public List getDisplayHookList() {
- ensureHookListCreated();
- return new ArrayList<>(displayHookList);
- }
-
- public Method getMethodForDisplayName(String displayName) {
- if (displayName == null || displayName.isEmpty())
+ public MethodHandle resolveHookHandle(String hookName, Object[] args, Object compiledInstance) {
+ if (hookName == null || hookName.isEmpty() || compiledInstance == null)
return null;
- ensureHookListCreated();
- return displayHookMethods.get(displayName);
- }
+ Object[] safeArgs = args == null ? new Object[0] : args;
+ String signatureKey = buildSignatureKey(hookName, safeArgs);
- /**
- * Builds the human-readable hook list and display-name -> Method map.
- * The list is computed lazily on first access and cached.
- */
- private void ensureHookListCreated() {
- if (hookListInitialized)
- return;
+ Class> compiledClass = compiledInstance.getClass();
- Map> grouped = Arrays.stream(interfaceType.getDeclaredMethods())
- .collect(Collectors.groupingBy(Method::getName));
+ // Clear cache if compiled class changed (recompilation)
+ if (lastCompiledClass != compiledClass) {
+ compiledClassCache.clear();
+ compiledHandleCache.clear();
+ methodToHandle.clear();
+ lastCompiledClass = compiledClass;
+ }
- for (Map.Entry> entry : grouped.entrySet()) {
- List methods = entry.getValue();
+ // Check handle cache
+ MethodHandle handle = compiledHandleCache.get(signatureKey);
+ if (handle != null)
+ return handle;
- methods.sort(Comparator.comparingInt(Method::getParameterCount));
+ // Resolve Method first
+ Method method = resolveHookMethod(hookName, args, compiledInstance);
+ if (method == null)
+ return null;
- boolean first = true;
- for (Method m : methods) {
- if (Modifier.isFinal(m.getModifiers()))
- continue;
-
- String displayName = first ? m.getName() :
- m.getName() + "(" + Arrays.stream(m.getParameterTypes())
- .map(Class::getSimpleName)
- .collect(Collectors.joining(", ")) + ")";
-
- displayHookList.add(displayName);
- displayHookMethods.put(displayName, m);
- first = false;
+ // Convert Method to MethodHandle (deduplicated by Method identity)
+ handle = methodToHandle.get(method);
+ if (handle == null) {
+ try {
+ handle = LOOKUP.unreflect(method);
+ methodToHandle.put(method, handle);
+ } catch (IllegalAccessException e) {
+ return null;
}
}
- hookListInitialized = true;
+ compiledHandleCache.put(signatureKey, handle);
+ return handle;
}
/**
- * Maps EnumScriptType common names to their respective methods using
- * the {@code @ScriptHook} annotation.
+ * Build a cache key for a hook name and runtime argument list.
*/
- private void initializeEnumHookMethods() {
- for (Method method : interfaceType.getDeclaredMethods()) {
- ScriptHook annotation = method.getAnnotation(ScriptHook.class);
- if (annotation == null)
- continue;
-
- for (EnumScriptType hookType : annotation.value()) {
- enumHookLookup.put(hookType.function, method);
- }
- }
- }
-
- /**
- * Build a cache key for a hook name and runtime argument list. This key
- * encodes the runtime classes of the provided arguments and is used for
- * quick cache lookups. Note: keys are based on concrete runtime classes
- * which may cause fragmentation when many subclasses are used.
- *
- * @param hookName hook name
- * @param args runtime arguments
- * @return a string key representing the hook signature
- */
- public static String buildSignatureKey(String hookName, Object[] args) {
+ private static String buildSignatureKey(String hookName, Object[] args) {
StringBuilder sb = new StringBuilder();
sb.append(hookName).append('#');
@@ -284,15 +183,7 @@ public final class JaninoHookResolver {
/**
* Internal resolution routine. Attempts to find the best matching method
* on the provided class by comparing parameter counts and a simple
- * match scoring function (see {@link #scoreMethodMatch}).
- *
- *
This searches all public methods (including inherited) to support
- * both interface-defined hooks and user-defined custom methods.
- *
- * @param targetClass the class to search (interface or compiled class)
- * @param hookName the method name to find
- * @param args runtime arguments for overload resolution
- * @return the best matching method, or null if none found
+ * match scoring function.
*/
private static Method resolveHookMethodInternal(Class> targetClass, String hookName, Object[] args) {
Object[] safeArgs = args == null ? new Object[0] : args;
@@ -300,7 +191,6 @@ public final class JaninoHookResolver {
Method best = null;
int bestScore = Integer.MAX_VALUE;
- // Use getMethods() to include inherited methods (important for compiled classes)
for (Method method : targetClass.getMethods()) {
if (!method.getName().equals(hookName))
continue;
@@ -331,13 +221,8 @@ public final class JaninoHookResolver {
}
/**
- * Score how well a method's parameter types match the provided runtime
- * arguments. Lower scores are better; {@link Integer#MAX_VALUE} indicates
- * an incompatible parameter.
- *
- * @param paramTypes method parameter types
- * @param args runtime argument values
- * @return score (lower is better) or Integer.MAX_VALUE for no match
+ * Score how well a method's parameter types match the provided runtime arguments.
+ * Lower scores are better; Integer.MAX_VALUE indicates an incompatible parameter.
*/
private static int scoreMethodMatch(Class>[] paramTypes, Object[] args) {
int score = 0;
@@ -392,16 +277,6 @@ public final class JaninoHookResolver {
return type;
}
- /**
- * Compute a simple inheritance distance between a child and parent class
- * by walking superclasses. Returns a large value if {@code parent} is
- * not assignable from {@code child}.
- *
- * Note: the matching algorithm uses superclass traversal to compute a
- * distance metric; interface inheritance is not explored which can make
- * matches against interfaces less optimal. Consider improving
- * `inheritanceDistance` if interface-based scoring is required.
- */
private static int inheritanceDistance(Class> child, Class> parent) {
if (child.equals(parent))
return 0;
diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java
index 6f6d6e55b..8093b8224 100644
--- a/src/main/java/noppes/npcs/janino/JaninoScript.java
+++ b/src/main/java/noppes/npcs/janino/JaninoScript.java
@@ -12,18 +12,15 @@ import noppes.npcs.constants.EnumScriptType;
import noppes.npcs.controllers.ScriptController;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.data.IScriptUnit;
-import noppes.npcs.janino.annotations.ParamName;
import org.codehaus.commons.compiler.InternalCompilerException;
import org.codehaus.commons.compiler.Sandbox;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
+import java.lang.invoke.MethodHandle;
import java.security.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
-import java.util.stream.Collectors;
public abstract class JaninoScript implements IScriptUnit {
public boolean errored = false;
@@ -286,26 +283,27 @@ public abstract class JaninoScript implements IScriptUnit {
if (hookName == null || hookName.isEmpty())
return null;
- // Ensure compiled first so we can search the compiled class
ensureCompiled();
T t = getUnsafe();
if (t == null)
return null;
- // Pass compiled instance to enable dynamic hook resolution
- // This allows users to define methods for ANY hook (Forge events, addon hooks, etc.)
- Method method = hookResolver.resolveHookMethod(hookName, args, t);
- if (method == null)
- return null;
-
- Method invokeMethod = method;
Object[] invokeArgs = args == null ? new Object[0] : args;
+ MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, t);
+ if (handle == null)
+ return null;
+
+ final MethodHandle finalHandle = handle;
try {
return sandbox.confine((PrivilegedAction