Test Janino

This commit is contained in:
Kam committed 2026-01-22 16:24:48 -05:00
1 parent 73f0d4e441
commit e6221c0940
3 files changed
+184 -192

No files matched your search

@@ -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.
*
* <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.
*/
public final class JaninoHookResolver {
private final Class<?> interfaceType;
@@ -30,6 +39,18 @@ public final class JaninoHookResolver {
*/
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.
*/
private final Map<String, Method> 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.
*
* <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>
*
* <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>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 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}).
*
* <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
*/
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();
@@ -202,15 +202,18 @@ public abstract class JaninoScript<T> 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;
@@ -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.
*
* <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>
*/
public class JaninoPlayerScript extends JaninoScript<JaninoPlayerScript.Functions> {
@@ -23,236 +45,102 @@ public class JaninoPlayerScript extends JaninoScript<JaninoPlayerScript.Function
}
/**
* Interface defining the hook methods available for player scripts.
* Each method corresponds to a player hook from ScriptHookController CONTEXT_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
// ==================== CORE LIFECYCLE ====================
@ScriptHook(EnumScriptType.INIT)
default void init(@ParamName("event") IPlayerEvent.InitEvent event) {}
@ScriptHook(EnumScriptType.TICK)
default void tick(@ParamName("event") IPlayerEvent.UpdateEvent event) {}
// Interaction
@ScriptHook(EnumScriptType.INTERACT)
default void interact(@ParamName("event") IPlayerEvent.InteractEvent event) {}
@ScriptHook(EnumScriptType.TIMER)
default void timer(@ParamName("event") IPlayerEvent.TimerEvent event) {}
@ScriptHook(EnumScriptType.RIGHT_CLICK)
default void rightClick(@ParamName("event") IPlayerEvent.RightClickEvent event) {}
// Combat
// ==================== 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_ENTITY)
default void damagedEntity(@ParamName("event") IPlayerEvent.DamagedEntityEvent 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) {}
// Inventory and drops
@ScriptHook(EnumScriptType.DROP)
default void drop(@ParamName("event") IPlayerEvent.DropEvent event) {}
// ==================== INTERACTION ====================
@ScriptHook(EnumScriptType.INTERACT)
default void interact(@ParamName("event") IPlayerEvent.InteractEvent event) {}
@ScriptHook(EnumScriptType.RESPAWN)
default void respawn(@ParamName("event") IPlayerEvent.RespawnEvent 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) {}
// Communication
@ScriptHook(EnumScriptType.CHAT)
default void chat(@ParamName("event") IPlayerEvent.ChatEvent 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) {}
// Item interactions
@ScriptHook(EnumScriptType.TOSS)
default void toss(@ParamName("event") IPlayerEvent.TossEvent 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.PICKUP_XP)
default void pickupXP(@ParamName("event") IPlayerEvent.PickupXPEvent event) {}
@ScriptHook(EnumScriptType.TOSS)
default void toss(@ParamName("event") IPlayerEvent.TossEvent event) {}
@ScriptHook(EnumScriptType.RANGED_CHARGE)
default void rangedCharge(@ParamName("event") IPlayerEvent.RangedChargeEvent event) {}
@ScriptHook(EnumScriptType.DROP)
default void drop(@ParamName("event") IPlayerEvent.DropEvent event) {}
@ScriptHook(EnumScriptType.RANGED_LAUNCHED)
default void rangedLaunched(@ParamName("event") IPlayerEvent.RangedLaunchedEvent event) {}
@ScriptHook(EnumScriptType.TIMER)
default void timer(@ParamName("event") IPlayerEvent.TimerEvent event) {}
// Item usage
@ScriptHook(EnumScriptType.START_USING_ITEM)
default void startItem(@ParamName("event") IPlayerEvent.StartUsingItem event) {}
@ScriptHook(EnumScriptType.USING_ITEM)
default void usingItem(@ParamName("event") IPlayerEvent.UsingItem event) {}
@ScriptHook(EnumScriptType.STOP_USING_ITEM)
default void stopItem(@ParamName("event") IPlayerEvent.StopUsingItem event) {}
@ScriptHook(EnumScriptType.FINISH_USING_ITEM)
default void finishItem(@ParamName("event") IPlayerEvent.FinishUsingItem event) {}
// Containers and world interaction
@ScriptHook(EnumScriptType.CONTAINER_OPEN)
default void containerOpen(@ParamName("event") IPlayerEvent.ContainerOpen event) {}
@ScriptHook(EnumScriptType.USE_HOE)
default void useHoe(@ParamName("event") IPlayerEvent.UseHoeEvent event) {}
@ScriptHook(EnumScriptType.BONEMEAL)
default void bonemeal(@ParamName("event") IPlayerEvent.BonemealEvent event) {}
@ScriptHook(EnumScriptType.FILL_BUCKET)
default void fillBucket(@ParamName("event") IPlayerEvent.FillBucketEvent event) {}
// Movement and status
// ==================== 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.WAKE_UP)
default void wakeUp(@ParamName("event") IPlayerEvent.WakeUpEvent event) {}
@ScriptHook(EnumScriptType.SLEEP)
default void sleep(@ParamName("event") IPlayerEvent.SleepEvent event) {}
@ScriptHook(EnumScriptType.PLAYSOUND)
default void playSound(@ParamName("event") IPlayerEvent.SoundEvent event) {}
@ScriptHook(EnumScriptType.LIGHTNING)
default void lightning(@ParamName("event") IPlayerEvent.LightningEvent event) {}
@ScriptHook(EnumScriptType.CHANGED_DIM)
default void changedDim(@ParamName("event") IPlayerEvent.ChangedDimension event) {}
// Quest
@ScriptHook(EnumScriptType.QUEST_START)
default void questStart(@ParamName("event") IQuestEvent.QuestStartEvent event) {}
@ScriptHook(EnumScriptType.QUEST_COMPLETED)
default void questCompleted(@ParamName("event") IQuestEvent.QuestCompletedEvent event) {}
@ScriptHook(EnumScriptType.QUEST_TURNIN)
default void questTurnIn(@ParamName("event") IQuestEvent.QuestTurnedInEvent event) {}
// Faction
@ScriptHook(EnumScriptType.FACTION_POINTS)
default void factionPoints(@ParamName("event") IFactionEvent.FactionPoints event) {}
// Dialog
@ScriptHook(EnumScriptType.DIALOG_OPEN)
default void dialogOpen(@ParamName("event") IDialogEvent.DialogOpen event) {}
@ScriptHook(EnumScriptType.DIALOG_OPTION)
default void dialogOption(@ParamName("event") IDialogEvent.DialogOption event) {}
@ScriptHook(EnumScriptType.DIALOG_CLOSE)
default void dialogClose(@ParamName("event") IDialogEvent.DialogClosed event) {}
// Commands
@ScriptHook(EnumScriptType.SCRIPT_COMMAND)
default void scriptCommand(@ParamName("event") ICustomNPCsEvent.ScriptedCommandEvent event) {}
// Custom GUI
@ScriptHook(EnumScriptType.CUSTOM_GUI_CLOSED)
default void customGuiClosed(@ParamName("event") ICustomGuiEvent.CloseEvent event) {}
@ScriptHook(EnumScriptType.CUSTOM_GUI_BUTTON)
default void customGuiButton(@ParamName("event") ICustomGuiEvent.ButtonEvent event) {}
@ScriptHook(EnumScriptType.CUSTOM_GUI_SLOT)
default void customGuiSlot(@ParamName("event") ICustomGuiEvent.SlotEvent event) {}
@ScriptHook(EnumScriptType.CUSTOM_GUI_SLOT_CLICKED)
default void customGuiSlotClicked(@ParamName("event") ICustomGuiEvent.SlotClickEvent event) {}
@ScriptHook(EnumScriptType.CUSTOM_GUI_SCROLL)
default void customGuiScroll(@ParamName("event") ICustomGuiEvent.ScrollEvent event) {}
@ScriptHook(EnumScriptType.CUSTOM_GUI_TEXTFIELD)
default void customGuiTextfield(@ParamName("event") ICustomGuiEvent.UnfocusedEvent event) {}
// Party
@ScriptHook(EnumScriptType.PARTY_QUEST_COMPLETED)
default void partyQuestCompleted(@ParamName("event") IPartyEvent.PartyQuestCompletedEvent event) {}
@ScriptHook(EnumScriptType.PARTY_QUEST_SET)
default void partyQuestSet(@ParamName("event") IPartyEvent.PartyQuestSetEvent event) {}
@ScriptHook(EnumScriptType.PARTY_QUEST_TURNED_IN)
default void partyQuestTurnedIn(@ParamName("event") IPartyEvent.PartyQuestTurnedInEvent event) {}
@ScriptHook(EnumScriptType.PARTY_INVITE)
default void partyInvite(@ParamName("event") IPartyEvent.PartyInviteEvent event) {}
@ScriptHook(EnumScriptType.PARTY_KICK)
default void partyKick(@ParamName("event") IPartyEvent.PartyKickEvent event) {}
@ScriptHook(EnumScriptType.PARTY_LEAVE)
default void partyLeave(@ParamName("event") IPartyEvent.PartyLeaveEvent event) {}
@ScriptHook(EnumScriptType.PARTY_DISBAND)
default void partyDisband(@ParamName("event") IPartyEvent.PartyDisbandEvent event) {}
// Animation
@ScriptHook(EnumScriptType.ANIMATION_START)
default void animationStart(@ParamName("event") IAnimationEvent.Started event) {}
@ScriptHook(EnumScriptType.ANIMATION_END)
default void animationEnd(@ParamName("event") IAnimationEvent.Ended event) {}
@ScriptHook(EnumScriptType.ANIMATION_FRAME_ENTER)
default void frameEnter(@ParamName("event") IAnimationEvent.IFrameEvent.Entered event) {}
@ScriptHook(EnumScriptType.ANIMATION_FRAME_EXIT)
default void frameExit(@ParamName("event") IAnimationEvent.IFrameEvent.Exited event) {}
// Profile
@ScriptHook(EnumScriptType.PROFILE_CHANGE)
default void profileChange(@ParamName("event") IPlayerEvent.ProfileEvent.Changed event) {}
@ScriptHook(EnumScriptType.PROFILE_REMOVE)
default void profileRemove(@ParamName("event") IPlayerEvent.ProfileEvent.Removed event) {}
@ScriptHook(EnumScriptType.PROFILE_CREATE)
default void profileCreate(@ParamName("event") IPlayerEvent.ProfileEvent.Create event) {}
// Effects
@ScriptHook(EnumScriptType.ON_EFFECT_ADD)
default void onEffectAdd(@ParamName("event") IPlayerEvent.EffectEvent.Added event) {}
@ScriptHook(EnumScriptType.ON_EFFECT_TICK)
default void onEffectTick(@ParamName("event") IPlayerEvent.EffectEvent.Ticked event) {}
@ScriptHook(EnumScriptType.ON_EFFECT_REMOVE)
default void onEffectRemove(@ParamName("event") IPlayerEvent.EffectEvent.Removed 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!
// ====================================================================================
}
}