No @ScriptHook needed anymore

This commit is contained in:
Kam committed 2026-01-22 18:44:41 -05:00
1 parent bee4d18bc6
commit ee593c62c0
5 files changed
+108 -609

No files matched your search

@@ -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.
*
* <h3>Dynamic Hook Support</h3>
* This resolver supports two modes:
* <ul>
* <li><b>Interface-only</b>: Only methods defined in the interface are found (legacy behavior)</li>
* <li><b>Compiled class</b>: Methods on the actual compiled script class are found,
* allowing users to define custom methods for Forge events, addon hooks, etc.</li>
* </ul>
* 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<String, Method> 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<String, Method> 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<String, Method> 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<String> displayHookList = new ArrayList<>();
/* Display name -> Method map */
private final Map<String, Method> displayHookMethods = new HashMap<>();
// MethodHandle caches
private final Map<String, MethodHandle> compiledHandleCache = new HashMap<>();
// Method -> MethodHandle for identity-based deduplication
private final Map<Method, MethodHandle> 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.
*
* <p>This enables <b>dynamic hook support</b>: users can define methods for any hook
* (Forge events, addon hooks, etc.) without needing them in the interface definition.</p>
* Resolve a hook method by name, searching the compiled class.
*
* <p>Resolution order:</p>
* <ol>
* <li>Enum-mapped methods (via {@code @ScriptHook})</li>
* <li>Compiled class methods (user-defined, including custom hooks)</li>
* <li>Interface methods (base hooks with defaults)</li>
* <li>Compiled class methods (user-defined)</li>
* <li>Fallback to no-arg versions</li>
* </ol>
*
* <p><b>Performance:</b> Method lookups are cached. First call per hook does reflection
* (~microseconds), subsequent calls use cache (essentially free).</p>
*
* @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<String> 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<String, List<Method>> 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<String, List<Method>> entry : grouped.entrySet()) {
List<Method> 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}).
*
* <p>This searches all public methods (including inherited) to support
* both interface-defined hooks and user-defined custom methods.</p>
*
* @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;
@@ -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<T> implements IScriptUnit {
public boolean errored = false;
@@ -286,26 +283,27 @@ public abstract class JaninoScript<T> 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<Object>) () -> {
try {
return invokeMethod.invoke(t, invokeArgs);
} catch (Exception e) {
// Prepend receiver to args for virtual method call
Object[] fullArgs = new Object[invokeArgs.length + 1];
fullArgs[0] = t;
System.arraycopy(invokeArgs, 0, fullArgs, 1, invokeArgs.length);
return finalHandle.invokeWithArguments(fullArgs);
} catch (Throwable e) {
appendConsole("Error calling hook " + hookName + ": " + e.getMessage());
return null;
}
@@ -416,127 +414,31 @@ public abstract class JaninoScript<T> implements IScriptUnit {
@Override
public String generateHookStub(String hookName, Object hookData) {
hookResolver.clearResolutionCaches();
// 1. Try interface method (existing @ScriptHook annotated methods)
Method method = hookResolver.getMethodForDisplayName(hookName);
if (method != null)
return generateMethodStub(method);
// 2. Look up in hook definition registry (addon hooks, dynamic hooks)
// Look up in hook definition registry
IHookDefinition def = getHookDefinition(hookName);
if (def != null) {
return generateStubFromDefinition(def);
}
String typeName = def.getUsableTypeName();
String[] params = def.paramNames();
String paramName = (params != null && params.length > 0) ? params[0] : "event";
// 3. Last resort: generic Object parameter
return String.format("public void %s(Object event) {\n \n}\n", hookName);
}
/**
* Generate a method stub from a hook definition.
*
* @param def The hook definition containing type info
* @return Generated stub code
*/
private String generateStubFromDefinition(IHookDefinition def) {
String typeName = def.getUsableTypeName();
String[] params = def.paramNames();
String paramName = (params != null && params.length > 0) ? params[0] : "event";
if (typeName == null || typeName.isEmpty()) {
return String.format("public void %s(Object %s) {\n \n}\n",
def.hookName(), paramName);
}
return String.format("public void %s(%s %s) {\n \n}\n",
def.hookName(), typeName, paramName);
}
/**
* Generates a stub string for a given Method.
* Handles nested classes properly (e.g., IPlayerEvent.InitEvent instead of just InitEvent).
*/
public static String generateMethodStub(Method method) {
String mods = Modifier.toString(method.getModifiers());
// Remove 'abstract' from modifiers for implementation
mods = mods.replace("abstract ", "").replace("abstract", "").trim();
if (!mods.isEmpty())
mods += " ";
String returnTypeStr = getUsableTypeName(method.getReturnType());
String name = method.getName();
Map<String, Integer> typeCount = new HashMap<>();
String params = Arrays.stream(method.getParameters())
.map(p -> {
String typeName = getUsableTypeName(p.getType());
String simpleTypeName = p.getType().getSimpleName();
// Check for @ParamName annotation first
ParamName paramNameAnnotation = p.getAnnotation(ParamName.class);
String paramName;
if (paramNameAnnotation != null) {
// Use annotated name
paramName = paramNameAnnotation.value();
} else {
// Fall back to generated name based on simple name
String baseName = Character.toLowerCase(simpleTypeName.charAt(0)) + simpleTypeName.substring(1);
int count = typeCount.getOrDefault(baseName, 0) + 1;
typeCount.put(baseName, count);
paramName = count == 1 ? baseName : baseName + (count - 1);
}
return typeName + " " + paramName;
})
.collect(Collectors.joining(", "));
String body;
Class<?> returnType = method.getReturnType();
if (returnType == void.class)
body = "";
else if (returnType.isPrimitive()) {
if (returnType == boolean.class)
body = " return false;";
else if (returnType == char.class)
body = " return '\\0';";
else
body = " return 0;";
} else {
body = " return null;";
}
return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body);
}
/**
* Gets a usable type name for stub generation.
* For nested classes like IPlayerEvent.InitEvent, returns "IPlayerEvent.InitEvent"
* For top-level classes, returns the simple name.
* For primitives, returns the primitive name.
*/
private static String getUsableTypeName(Class<?> type) {
if (type.isPrimitive() || type.getEnclosingClass() == null) {
return type.getSimpleName();
}
// Build the nested class chain: OuterClass.InnerClass.DeeperClass
StringBuilder sb = new StringBuilder();
Class<?> current = type;
while (current != null) {
if (sb.length() > 0) {
sb.insert(0, ".");
if (typeName != null && !typeName.isEmpty()) {
return String.format("public void %s(%s %s) {\n \n}\n",
def.hookName(), typeName, paramName);
}
sb.insert(0, current.getSimpleName());
current = current.getEnclosingClass();
}
return sb.toString();
// Fallback: generic Object parameter
return String.format("public void %s(Object event) {\n \n}\n", hookName);
}
public List<String> getHookList() {
return hookResolver.getDisplayHookList();
// Get hooks from registry based on context
String context = getHookContext();
if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) {
return ScriptHookController.Instance.getAllHooks(context);
}
return Collections.emptyList();
}
@@ -1,22 +0,0 @@
package noppes.npcs.janino.annotations;
import noppes.npcs.constants.EnumScriptType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to mark script hook methods and associate them with their EnumScriptType.
* Used by JaninoScript to automatically build a map of hook types to methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ScriptHook {
/**
* The script type(s) this method handles.
* Multiple types can be specified for methods that handle multiple hooks.
*/
EnumScriptType[] value();
}
@@ -1,35 +1,15 @@
package noppes.npcs.janino.impl;
import noppes.npcs.api.event.IAbilityEvent;
import noppes.npcs.api.event.INpcEvent;
import noppes.npcs.api.event.IProjectileEvent;
import noppes.npcs.constants.EnumScriptType;
import noppes.npcs.janino.JaninoScript;
import noppes.npcs.janino.annotations.ParamName;
import noppes.npcs.janino.annotations.ScriptHook;
/**
* Janino (Java) script implementation for NPC events.
* This allows NPCs to use compiled Java code instead of ECMAScript.
* Hook methods are resolved dynamically by name - no interface definition required.
*/
public class JaninoNpcScript extends JaninoScript<JaninoNpcScript.Functions> {
/**
* Default imports for NPC scripts.
* These are automatically available in user code without explicit import statements.
* Additional imports from registered hook definitions are added at compile time.
*/
private static final String[] DEFAULT_IMPORTS = {
"noppes.npcs.api.event.INpcEvent",
"noppes.npcs.api.event.IProjectileEvent",
"noppes.npcs.api.event.IAbilityEvent",
"noppes.npcs.api.entity.ICustomNpc",
"noppes.npcs.api.entity.IPlayer",
"noppes.npcs.api.NpcAPI"
};
public JaninoNpcScript() {
super(Functions.class, DEFAULT_IMPORTS);
super(Functions.class, null);
}
@Override
@@ -37,93 +17,5 @@ public class JaninoNpcScript extends JaninoScript<JaninoNpcScript.Functions> {
return "npc";
}
/**
* Interface defining the hook methods available for NPC scripts.
* Each method corresponds to an NPC event from ScriptHookController CONTEXT_NPC.
* Methods are annotated with @ScriptHook to map them to their EnumScriptType.
*/
public interface Functions {
// Core lifecycle events
@ScriptHook(EnumScriptType.INIT)
default void init(@ParamName("event") INpcEvent.InitEvent event) {}
@ScriptHook(EnumScriptType.TICK)
default void tick(@ParamName("event") INpcEvent.UpdateEvent event) {}
// Interaction events
@ScriptHook(EnumScriptType.INTERACT)
default void interact(@ParamName("event") INpcEvent.InteractEvent event) {}
@ScriptHook(EnumScriptType.DIALOG)
default void dialog(@ParamName("event") INpcEvent.DialogEvent event) {}
@ScriptHook(EnumScriptType.DIALOG_CLOSE)
default void dialogClose(@ParamName("event") INpcEvent.DialogClosedEvent event) {}
// Combat events - damage
@ScriptHook(EnumScriptType.DAMAGED)
default void damaged(@ParamName("event") INpcEvent.DamagedEvent event) {}
@ScriptHook(EnumScriptType.KILLED)
default void killed(@ParamName("event") INpcEvent.DiedEvent event) {}
@ScriptHook(EnumScriptType.KILLS)
default void kills(@ParamName("event") INpcEvent.KilledEntityEvent event) {}
// Combat events - attacks
@ScriptHook(EnumScriptType.ATTACK_MELEE)
default void meleeAttack(@ParamName("event") INpcEvent.MeleeAttackEvent event) {}
@ScriptHook(EnumScriptType.ATTACK_SWING)
default void meleeSwing(@ParamName("event") INpcEvent.SwingEvent event) {}
@ScriptHook(EnumScriptType.RANGED_LAUNCHED)
default void rangedLaunched(@ParamName("event") INpcEvent.RangedLaunchedEvent event) {}
// Targeting events
@ScriptHook(EnumScriptType.TARGET)
default void target(@ParamName("event") INpcEvent.TargetEvent event) {}
@ScriptHook(EnumScriptType.TARGET_LOST)
default void targetLost(@ParamName("event") INpcEvent.TargetLostEvent event) {}
// Other NPC events
@ScriptHook(EnumScriptType.COLLIDE)
default void collide(@ParamName("event") INpcEvent.CollideEvent event) {}
@ScriptHook(EnumScriptType.TIMER)
default void timer(@ParamName("event") INpcEvent.TimerEvent event) {}
// Projectile events
@ScriptHook(EnumScriptType.PROJECTILE_TICK)
default void projectileTick(@ParamName("event") IProjectileEvent.UpdateEvent event) {}
@ScriptHook(EnumScriptType.PROJECTILE_IMPACT)
default void projectileImpact(@ParamName("event") IProjectileEvent.ImpactEvent event) {}
// Ability events
@ScriptHook(EnumScriptType.ABILITY_START)
default void abilityStart(@ParamName("event") IAbilityEvent.StartEvent event) {}
@ScriptHook(EnumScriptType.ABILITY_EXECUTE)
default void abilityExecute(@ParamName("event") IAbilityEvent.ExecuteEvent event) {}
@ScriptHook(EnumScriptType.ABILITY_HIT)
default void abilityHit(@ParamName("event") IAbilityEvent.HitEvent event) {}
@ScriptHook(EnumScriptType.ABILITY_TICK)
default void abilityTick(@ParamName("event") IAbilityEvent.TickEvent event) {}
@ScriptHook(EnumScriptType.ABILITY_INTERRUPT)
default void abilityInterrupt(@ParamName("event") IAbilityEvent.InterruptEvent event) {}
@ScriptHook(EnumScriptType.ABILITY_COMPLETE)
default void abilityComplete(@ParamName("event") IAbilityEvent.CompleteEvent event) {}
}
public interface Functions {}
}
@@ -1,65 +1,15 @@
package noppes.npcs.janino.impl;
import noppes.npcs.api.event.IPlayerEvent;
import noppes.npcs.constants.EnumScriptType;
import noppes.npcs.janino.JaninoScript;
import noppes.npcs.janino.annotations.ParamName;
import noppes.npcs.janino.annotations.ScriptHook;
/**
* Janino (Java) script implementation for player events.
*
* <h3>Dynamic Hook Support</h3>
* 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:
*
* <pre>{@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) {
* // ...
* }
* }</pre>
*
* The interface below defines core hooks for:
* <ul>
* <li>EnumScriptType mapping (so "init" maps to init() method)</li>
* <li>GUI hook list display</li>
* <li>Stub generation with correct parameter types</li>
* </ul>
* Hook methods are resolved dynamically by name - no interface definition required.
*/
public class JaninoPlayerScript extends JaninoScript<JaninoPlayerScript.Functions> {
/**
* Default imports for player scripts.
* These are automatically available in user code without explicit import statements.
* Additional imports from registered hook definitions are added at compile time.
*/
private static final String[] DEFAULT_IMPORTS = {
"noppes.npcs.api.event.IPlayerEvent",
"noppes.npcs.api.event.IQuestEvent",
"noppes.npcs.api.event.IDialogEvent",
"noppes.npcs.api.event.IFactionEvent",
"noppes.npcs.api.event.IPartyEvent",
"noppes.npcs.api.event.ICustomGuiEvent",
"noppes.npcs.api.event.IAnimationEvent",
"noppes.npcs.api.event.ICustomNPCsEvent",
"noppes.npcs.api.entity.IPlayer",
"noppes.npcs.api.NpcAPI"
};
public JaninoPlayerScript() {
super(Functions.class, DEFAULT_IMPORTS, false);
super(Functions.class, null, false);
}
@Override
@@ -67,103 +17,5 @@ public class JaninoPlayerScript extends JaninoScript<JaninoPlayerScript.Function
return "player";
}
/**
* Core hook methods for player scripts.
*
* Note: Users can define additional methods beyond this interface!
* Any public method matching a hook name will be found via reflection.
*/
public interface Functions {
// ==================== CORE LIFECYCLE ====================
@ScriptHook(EnumScriptType.INIT)
default void init(@ParamName("event") IPlayerEvent.InitEvent event) {}
@ScriptHook(EnumScriptType.TICK)
default void tick(@ParamName("event") IPlayerEvent.UpdateEvent event) {}
@ScriptHook(EnumScriptType.TIMER)
default void timer(@ParamName("event") IPlayerEvent.TimerEvent event) {}
// ==================== COMBAT ====================
@ScriptHook(EnumScriptType.ATTACK)
default void attack(@ParamName("event") IPlayerEvent.AttackEvent event) {}
@ScriptHook(EnumScriptType.ATTACKED)
default void attacked(@ParamName("event") IPlayerEvent.AttackedEvent event) {}
@ScriptHook(EnumScriptType.DAMAGED)
default void damaged(@ParamName("event") IPlayerEvent.DamagedEvent event) {}
@ScriptHook(EnumScriptType.DAMAGED_ENTITY)
default void damagedEntity(@ParamName("event") IPlayerEvent.DamagedEntityEvent event) {}
@ScriptHook(EnumScriptType.KILLS)
default void kills(@ParamName("event") IPlayerEvent.KilledEntityEvent event) {}
@ScriptHook(EnumScriptType.KILLED)
default void killed(@ParamName("event") IPlayerEvent.DiedEvent event) {}
// ==================== INTERACTION ====================
@ScriptHook(EnumScriptType.INTERACT)
default void interact(@ParamName("event") IPlayerEvent.InteractEvent event) {}
@ScriptHook(EnumScriptType.RIGHT_CLICK)
default void rightClick(@ParamName("event") IPlayerEvent.RightClickEvent event) {}
@ScriptHook(EnumScriptType.BREAK_BLOCK)
default void breakBlock(@ParamName("event") IPlayerEvent.BreakEvent event) {}
// ==================== CONNECTION ====================
@ScriptHook(EnumScriptType.LOGIN)
default void login(@ParamName("event") IPlayerEvent.LoginEvent event) {}
@ScriptHook(EnumScriptType.LOGOUT)
default void logout(@ParamName("event") IPlayerEvent.LogoutEvent event) {}
@ScriptHook(EnumScriptType.RESPAWN)
default void respawn(@ParamName("event") IPlayerEvent.RespawnEvent event) {}
// ==================== INPUT ====================
@ScriptHook(EnumScriptType.KEY_PRESSED)
default void keyPressed(@ParamName("event") IPlayerEvent.KeyPressedEvent event) {}
@ScriptHook(EnumScriptType.MOUSE_CLICKED)
default void mouseClicked(@ParamName("event") IPlayerEvent.MouseClickedEvent event) {}
@ScriptHook(EnumScriptType.CHAT)
default void chat(@ParamName("event") IPlayerEvent.ChatEvent event) {}
// ==================== ITEMS ====================
@ScriptHook(EnumScriptType.PICKUP)
default void pickUp(@ParamName("event") IPlayerEvent.PickUpEvent event) {}
@ScriptHook(EnumScriptType.TOSS)
default void toss(@ParamName("event") IPlayerEvent.TossEvent event) {}
@ScriptHook(EnumScriptType.DROP)
default void drop(@ParamName("event") IPlayerEvent.DropEvent event) {}
// ==================== MOVEMENT ====================
@ScriptHook(EnumScriptType.JUMP)
default void jump(@ParamName("event") IPlayerEvent.JumpEvent event) {}
@ScriptHook(EnumScriptType.FALL)
default void fall(@ParamName("event") IPlayerEvent.FallEvent event) {}
@ScriptHook(EnumScriptType.CHANGED_DIM)
default void changedDim(@ParamName("event") IPlayerEvent.ChangedDimension event) {}
// ====================================================================================
// ALL OTHER HOOKS WORK VIA DYNAMIC RESOLUTION!
// ====================================================================================
// Users can define methods like:
// public void questStart(IQuestEvent.QuestStartEvent event) {}
// public void dialogOpen(IDialogEvent.DialogOpen event) {}
// public void customGuiButton(ICustomGuiEvent.ButtonEvent event) {}
// public void partyInvite(IPartyEvent.PartyInviteEvent event) {}
// public void myAddonHook(AddonEvent event) {}
//
// These will be found automatically by the JaninoHookResolver!
// ====================================================================================
}
public interface Functions {}
}