diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index 2c8eeb543..b77233b12 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -16,6 +16,15 @@ import java.util.stream.Collectors; * The resolver keeps simple caches for previously resolved signatures and * supports mapping enum-based hook names (via the {@code @ScriptHook} * annotation) to methods. + * + *
This enables dynamic hook support: users can define methods for any hook + * (Forge events, addon hooks, etc.) without needing them in the interface definition.
+ * + *Resolution order:
+ *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 args runtime arguments to use for overload resolution + * @param compiledInstance the compiled script instance (or null for interface-only) + * @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()) 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(); + + // 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; + } + } + + // 3. Fall back to interface-only search method = signatureCache.get(signatureKey); if (method != null) return method; @@ -197,17 +283,32 @@ public final class JaninoHookResolver { /** * Internal resolution routine. Attempts to find the best matching method - * on the provided interface by comparing parameter counts and a simple + * 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 */ - private static Method resolveHookMethodInternal(Class> interfaceType, String hookName, Object[] args) { + private static Method resolveHookMethodInternal(Class> targetClass, String hookName, Object[] args) { Object[] safeArgs = args == null ? new Object[0] : args; Method best = null; int bestScore = Integer.MAX_VALUE; - for (Method method : interfaceType.getDeclaredMethods()) { - if (!method.getName().equals(hookName) || Modifier.isFinal(method.getModifiers())) + // Use getMethods() to include inherited methods (important for compiled classes) + for (Method method : targetClass.getMethods()) { + if (!method.getName().equals(hookName)) + continue; + + // Skip final methods and Object methods + if (Modifier.isFinal(method.getModifiers())) + continue; + if (method.getDeclaringClass() == Object.class) continue; Class>[] paramTypes = method.getParameterTypes(); diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a2202271e..955aed3bc 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -202,15 +202,18 @@ public abstract class JaninoScript{@code
+ * // Standard hook (defined in interface)
+ * public void init(IPlayerEvent.InitEvent event) {
+ * // ...
+ * }
+ *
+ * // Custom addon hook (NOT in interface - still works!)
+ * public void myAddonHook(MyAddonEvent event) {
+ * // ...
+ * }
+ *
+ * // Any hook by name
+ * public void someCustomHook(Object event) {
+ * // ...
+ * }
+ * }
+ *
+ * The interface below defines core hooks for:
+ *