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. + * + *

Dynamic Hook Support

+ * This resolver supports two modes: + * + * Use {@link #resolveHookMethod(String, Object[], Object)} to search the compiled instance. */ public final class JaninoHookResolver { private final Class interfaceType; @@ -30,6 +39,18 @@ public final class JaninoHookResolver { */ private final Map signatureCache = new HashMap<>(); + /** + * Cache for methods found on compiled classes (keyed by class + signature). + * Separate from signatureCache to handle recompilation. + */ + private final Map compiledClassCache = new HashMap<>(); + + /** + * Track which compiled class we've cached methods for. + * If the class changes (recompilation), we clear the cache. + */ + 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) */ @@ -52,10 +73,12 @@ public final class JaninoHookResolver { */ public void clearResolutionCaches() { signatureCache.clear(); + compiledClassCache.clear(); + lastCompiledClass = null; } /** - * Resolve a hook method by name and runtime argument values. + * 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 @@ -66,16 +89,79 @@ public final class JaninoHookResolver { * @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.

+ * + *

Resolution order:

+ *
    + *
  1. Enum-mapped methods (via {@code @ScriptHook})
  2. + *
  3. Compiled class methods (user-defined, including custom hooks)
  4. + *
  5. Interface methods (base hooks with defaults)
  6. + *
  7. Fallback to no-arg versions
  8. + *
+ * + *

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 implements IScriptUnit { if (hookName == null || hookName.isEmpty()) return null; - Method method = hookResolver.resolveHookMethod(hookName, args); - if (method == null) - 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; diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index 0e1f3b004..10cb6d556 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -1,13 +1,6 @@ package noppes.npcs.janino.impl; -import noppes.npcs.api.event.IAnimationEvent; -import noppes.npcs.api.event.ICustomGuiEvent; -import noppes.npcs.api.event.ICustomNPCsEvent; -import noppes.npcs.api.event.IDialogEvent; -import noppes.npcs.api.event.IFactionEvent; -import noppes.npcs.api.event.IPartyEvent; import noppes.npcs.api.event.IPlayerEvent; -import noppes.npcs.api.event.IQuestEvent; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.janino.JaninoScript; import noppes.npcs.janino.annotations.ParamName; @@ -15,6 +8,35 @@ import noppes.npcs.janino.annotations.ScriptHook; /** * Janino (Java) script implementation for player events. + * + *

Dynamic Hook Support

+ * With the new dynamic hook resolution, users can define methods for ANY hook - + * not just those defined in the Functions interface. Simply write a public method + * with the hook name and matching parameter type: + * + *
{@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: + *
    + *
  • EnumScriptType mapping (so "init" maps to init() method)
  • + *
  • GUI hook list display
  • + *
  • Stub generation with correct parameter types
  • + *
*/ public class JaninoPlayerScript extends JaninoScript { @@ -23,236 +45,102 @@ public class JaninoPlayerScript extends JaninoScript