mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 06:07:27 +00:00
No @ScriptHook needed anymore
This commit is contained in:
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 {}
|
||||
}
|
||||
Reference in new issue
Block a user