diff --git a/src/api b/src/api index b90df20fc..7a337a570 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit b90df20fc42f11f7ff69e79792881ebc209735eb +Subproject commit 7a337a5704c442fb98a0fd4e0ba26210cb54af98 diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java new file mode 100644 index 000000000..b8b78fe70 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -0,0 +1,307 @@ +package noppes.npcs.controllers; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import noppes.npcs.api.handler.IHookDefinition; +import noppes.npcs.janino.annotations.ParamName; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +/** + * Implementation of {@link IHookDefinition} with a fluent builder API. + * + *

Usage Examples

+ *
{@code
+ * // Full definition with class reference
+ * HookDefinition.builder("onDBCTransform")
+ *     .eventClass(IDBCEvent.TransformEvent.class)
+ *     .paramNames("event")
+ *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ *     .cancelable(true)
+ *     .build();
+ *
+ * // String-based (avoids classloading issues)
+ * HookDefinition.builder("onDBCPowerUp")
+ *     .eventClass("com.dbc.api.event.IDBCEvent$PowerUpEvent")
+ *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ *     .build();
+ *
+ * // Simple hook (backward compat, no metadata)
+ * HookDefinition.simple("legacyHook");
+ * }
+ */ +public class HookDefinition implements IHookDefinition { + + private final String hookName; + private final String eventClassName; + private final String[] paramNames; + private final String[] requiredImports; + private final boolean cancelable; + + // Cached resolved class (transient - not serialized) + private transient Class cachedEventClass; + private transient boolean classResolutionAttempted; + + private HookDefinition(Builder builder) { + this.hookName = builder.hookName; + this.eventClassName = builder.eventClassName; + this.paramNames = builder.paramNames != null ? builder.paramNames : new String[]{"event"}; + this.requiredImports = builder.requiredImports != null ? builder.requiredImports : new String[0]; + this.cancelable = builder.cancelable; + } + + // ==================== IHookDefinition Implementation ==================== + + @Override + public String hookName() { + return hookName; + } + + @Override + public String eventClassName() { + return eventClassName; + } + + @Override + public String[] paramNames() { + return paramNames; + } + + @Override + public String[] requiredImports() { + return requiredImports; + } + + @Override + public boolean isCancelable() { + return cancelable; + } + + @Override + public Class getEventClass() { + if (classResolutionAttempted) { + return cachedEventClass; + } + + classResolutionAttempted = true; + if (eventClassName == null || eventClassName.isEmpty()) { + return null; + } + + try { + cachedEventClass = Class.forName(eventClassName); + } catch (ClassNotFoundException e) { + cachedEventClass = null; + } + + return cachedEventClass; + } + + // ==================== Factory Methods ==================== + + /** + * Create a new builder for a hook definition. + * + * @param hookName The hook function name + * @return A new builder instance + */ + public static Builder builder(String hookName) { + return new Builder(hookName); + } + + /** + * Create a simple hook definition with just a name (no metadata). + * For backward compatibility with legacy hook registration. + * + * @param hookName The hook function name + * @return A minimal hook definition + */ + public static HookDefinition simple(String hookName) { + return builder(hookName).build(); + } + + /** + * Create a hook definition from an interface method. + * Automatically extracts metadata from the method signature and annotations. + * + * @param hookName The hook name (may differ from method name) + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(String hookName, Method method) { + Builder builder = builder(hookName); + + // Extract event type from first parameter + if (method.getParameterCount() > 0) { + Class eventType = method.getParameterTypes()[0]; + builder.eventClass(eventType); + + // Extract @Cancelable annotation + if (eventType.isAnnotationPresent(Cancelable.class)) { + builder.cancelable(true); + } + + // Build required imports from event type + String importName = getImportForClass(eventType); + if (importName != null) { + builder.requiredImports(importName); + } + } + + // Extract parameter names from @ParamName annotations + Parameter[] params = method.getParameters(); + if (params.length > 0) { + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + ParamName annotation = params[i].getAnnotation(ParamName.class); + if (annotation != null) { + names[i] = annotation.value(); + } else { + names[i] = params[i].getType().getSimpleName().toLowerCase(); + } + } + builder.paramNames(names); + } + + return builder.build(); + } + + /** + * Get the import statement needed for a class. + * For nested classes, returns the enclosing class. + */ + private static String getImportForClass(Class clazz) { + if (clazz == null || clazz.isPrimitive()) { + return null; + } + + // For nested classes, get the top-level enclosing class + Class enclosing = clazz; + while (enclosing.getEnclosingClass() != null) { + enclosing = enclosing.getEnclosingClass(); + } + + String name = enclosing.getName(); + // Skip java.lang classes + if (name.startsWith("java.lang.")) { + return null; + } + + return name; + } + + // ==================== Builder ==================== + + /** + * Builder for creating HookDefinition instances with a fluent API. + */ + public static class Builder { + private final String hookName; + private String eventClassName; + private String[] paramNames; + private String[] requiredImports; + private boolean cancelable; + + private Builder(String hookName) { + if (hookName == null || hookName.isEmpty()) { + throw new IllegalArgumentException("hookName cannot be null or empty"); + } + this.hookName = hookName; + } + + /** + * Set the event class by name (avoids classloading issues). + * + * @param className Full qualified class name (use $ for nested classes) + * @return this builder + */ + public Builder eventClass(String className) { + this.eventClassName = className; + return this; + } + + /** + * Set the event class directly (convenience method). + * The class name will be extracted automatically. + * + * @param clazz The event class + * @return this builder + */ + public Builder eventClass(Class clazz) { + if (clazz != null) { + this.eventClassName = clazz.getName(); + // Auto-detect cancelable + if (clazz.isAnnotationPresent(Cancelable.class)) { + this.cancelable = true; + } + } + return this; + } + + /** + * Set parameter names for stub generation. + * + * @param names Parameter names + * @return this builder + */ + public Builder paramNames(String... names) { + this.paramNames = names; + return this; + } + + /** + * Set required imports for this hook's event type. + * + * @param imports Fully qualified class names to import + * @return this builder + */ + public Builder requiredImports(String... imports) { + this.requiredImports = imports; + return this; + } + + /** + * Set whether the event is cancelable. + * + * @param cancelable true if the event can be canceled + * @return this builder + */ + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + return this; + } + + /** + * Build the hook definition. + * + * @return A new HookDefinition instance + */ + public HookDefinition build() { + return new HookDefinition(this); + } + } + + // ==================== Object Methods ==================== + + @Override + public String toString() { + return "HookDefinition{" + + "hookName='" + hookName + '\'' + + ", eventClassName='" + eventClassName + '\'' + + ", cancelable=" + cancelable + + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof HookDefinition)) return false; + HookDefinition other = (HookDefinition) obj; + return hookName.equals(other.hookName); + } + + @Override + public int hashCode() { + return hookName.hashCode(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index c5af0929b..abefb0930 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,5 +1,6 @@ package noppes.npcs.controllers; +import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; @@ -14,17 +15,33 @@ import java.util.Map; /** * Controller for managing script hooks. * Allows addon mods to register custom hooks that appear in script editor GUIs. + * + *

Rich Hook Registration

+ * Supports full hook definitions with metadata for proper stub generation: + *
{@code
+ * ScriptHookController.Instance.registerHookDefinition("player",
+ *     HookDefinition.builder("onDBCTransform")
+ *         .eventClass(IDBCEvent.TransformEvent.class)
+ *         .requiredImports("com.dbc.api.event.IDBCEvent")
+ *         .build());
+ * }
*/ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Built-in hooks per context + // Built-in hooks per context (legacy string list) private final Map> builtInHooks = new HashMap<>(); - // Addon-registered hooks per context + // Addon-registered hooks per context (legacy string list) private final Map> addonHooks = new HashMap<>(); + // Rich hook definitions per context (NEW) + private final Map> hookDefinitions = new HashMap<>(); + + // Revision counter for cache invalidation (NEW) + private int hookRevision = 0; + public ScriptHookController() { Instance = this; initializeBuiltInHooks(); @@ -36,160 +53,24 @@ public class ScriptHookController implements IScriptHookHandler { if (!context.hookContext.isEmpty()) { builtInHooks.put(context.hookContext, new ArrayList<>()); addonHooks.put(context.hookContext, new ArrayList<>()); + hookDefinitions.put(context.hookContext, new HashMap<>()); } } - // NPC hooks - registerBuiltIn(ScriptContext.NPC, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.DIALOG.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLED.function, - EnumScriptType.ATTACK_MELEE.function, - EnumScriptType.ATTACK_SWING.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TARGET.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.KILLS.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.TIMER.function, - EnumScriptType.TARGET_LOST.function, - EnumScriptType.PROJECTILE_TICK.function, - EnumScriptType.PROJECTILE_IMPACT.function, - EnumScriptType.ABILITY_TICK.function, - EnumScriptType.ABILITY_EXECUTE.function, - EnumScriptType.ABILITY_HIT.function, - EnumScriptType.ABILITY_START.function, - EnumScriptType.ABILITY_INTERRUPT.function, - EnumScriptType.ABILITY_COMPLETE.function - ); + // NPC hooks with full definitions + initializeNpcHooks(); - // Player hooks - registerBuiltIn(ScriptContext.PLAYER, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.ATTACKED.function, - EnumScriptType.DAMAGED_ENTITY.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLS.function, - EnumScriptType.KILLED.function, - EnumScriptType.DROP.function, - EnumScriptType.RESPAWN.function, - EnumScriptType.BREAK_BLOCK.function, - EnumScriptType.CHAT.function, - EnumScriptType.LOGIN.function, - EnumScriptType.LOGOUT.function, - EnumScriptType.KEY_PRESSED.function, - EnumScriptType.MOUSE_CLICKED.function, - EnumScriptType.TOSS.function, - EnumScriptType.PICKUP.function, - EnumScriptType.PICKUP_XP.function, - EnumScriptType.RANGED_CHARGE.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TIMER.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function, - EnumScriptType.CONTAINER_OPEN.function, - EnumScriptType.USE_HOE.function, - EnumScriptType.BONEMEAL.function, - EnumScriptType.FILL_BUCKET.function, - EnumScriptType.JUMP.function, - EnumScriptType.FALL.function, - EnumScriptType.WAKE_UP.function, - EnumScriptType.SLEEP.function, - EnumScriptType.PLAYSOUND.function, - EnumScriptType.LIGHTNING.function, - EnumScriptType.CHANGED_DIM.function, - EnumScriptType.QUEST_START.function, - EnumScriptType.QUEST_COMPLETED.function, - EnumScriptType.QUEST_TURNIN.function, - EnumScriptType.FACTION_POINTS.function, - EnumScriptType.DIALOG_OPEN.function, - EnumScriptType.DIALOG_OPTION.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.SCRIPT_COMMAND.function, - EnumScriptType.CUSTOM_GUI_CLOSED.function, - EnumScriptType.CUSTOM_GUI_BUTTON.function, - EnumScriptType.CUSTOM_GUI_SLOT.function, - EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, - EnumScriptType.CUSTOM_GUI_SCROLL.function, - EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, - EnumScriptType.PARTY_QUEST_COMPLETED.function, - EnumScriptType.PARTY_QUEST_SET.function, - EnumScriptType.PARTY_QUEST_TURNED_IN.function, - EnumScriptType.PARTY_INVITE.function, - EnumScriptType.PARTY_KICK.function, - EnumScriptType.PARTY_LEAVE.function, - EnumScriptType.PARTY_DISBAND.function, - EnumScriptType.ANIMATION_START.function, - EnumScriptType.ANIMATION_END.function, - EnumScriptType.ANIMATION_FRAME_ENTER.function, - EnumScriptType.ANIMATION_FRAME_EXIT.function, - EnumScriptType.PROFILE_CHANGE.function, - EnumScriptType.PROFILE_REMOVE.function, - EnumScriptType.PROFILE_CREATE.function, - EnumScriptType.ON_EFFECT_ADD.function, - EnumScriptType.ON_EFFECT_TICK.function, - EnumScriptType.ON_EFFECT_REMOVE.function - ); + // Player hooks with full definitions + initializePlayerHooks(); - // Block hooks - registerBuiltIn(ScriptContext.BLOCK, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.FALLEN_UPON.function, - EnumScriptType.REDSTONE.function, - EnumScriptType.BROKEN.function, - EnumScriptType.EXPLODED.function, - EnumScriptType.RAIN_FILLED.function, - EnumScriptType.NEIGHBOR_CHANGED.function, - EnumScriptType.CLICKED.function, - EnumScriptType.HARVESTED.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.TIMER.function - ); + // Block hooks with full definitions + initializeBlockHooks(); - // Item hooks - registerBuiltIn(ScriptContext.ITEM, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); + // Item hooks with full definitions + initializeItemHooks(); - // Linked item hooks - registerBuiltIn(ScriptContext.LINKED_ITEM, - EnumScriptType.LINKED_ITEM_BUILD.function, - EnumScriptType.LINKED_ITEM_VERSION.function, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); + // Linked item hooks with full definitions + initializeLinkedItemHooks(); // Recipe hooks registerBuiltIn(ScriptContext.RECIPE, @@ -211,6 +92,372 @@ public class ScriptHookController implements IScriptHookHandler { ); } + private void initializeNpcHooks() { + String ctx = ScriptContext.NPC.hookContext; + String npcImport = "noppes.npcs.api.event.INpcEvent"; + String projImport = "noppes.npcs.api.event.IProjectileEvent"; + String abilityImport = "noppes.npcs.api.event.IAbilityEvent"; + + // Core lifecycle + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); + + // Interaction + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG.function, + "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, + "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, + "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); + + // Combat + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, + "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, + "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, + "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_MELEE.function, + "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_SWING.function, + "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, + "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TARGET.function, + "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TARGET_LOST.function, + "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); + + // Projectile events + registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_TICK.function, + "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_IMPACT.function, + "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); + + // Ability events + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_START.function, + "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_EXECUTE.function, + "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_HIT.function, + "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_TICK.function, + "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_INTERRUPT.function, + "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_COMPLETE.function, + "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); + } + + private void initializePlayerHooks() { + String ctx = ScriptContext.PLAYER.hookContext; + String playerImport = "noppes.npcs.api.event.IPlayerEvent"; + String questImport = "noppes.npcs.api.event.IQuestEvent"; + String dialogImport = "noppes.npcs.api.event.IDialogEvent"; + String factionImport = "noppes.npcs.api.event.IFactionEvent"; + String partyImport = "noppes.npcs.api.event.IPartyEvent"; + String guiImport = "noppes.npcs.api.event.ICustomGuiEvent"; + String animImport = "noppes.npcs.api.event.IAnimationEvent"; + + // Core lifecycle + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); + + // Combat + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACKED.function, + "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, + "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED_ENTITY.function, + "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, + "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, + "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); + + // Interaction + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.BREAK_BLOCK.function, + "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CHAT.function, + "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); + + // Connection + registerBuiltInDefinition(ctx, EnumScriptType.LOGIN.function, + "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.LOGOUT.function, + "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RESPAWN.function, + "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); + + // Input + registerBuiltInDefinition(ctx, EnumScriptType.KEY_PRESSED.function, + "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.MOUSE_CLICKED.function, + "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); + + // Items + registerBuiltInDefinition(ctx, EnumScriptType.PICKUP.function, + "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSS.function, + "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DROP.function, + "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKUP_XP.function, + "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CONTAINER_OPEN.function, + "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); + + // Movement + registerBuiltInDefinition(ctx, EnumScriptType.JUMP.function, + "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FALL.function, + "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CHANGED_DIM.function, + "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); + + // World + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_CHARGE.function, + "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, + "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.USE_HOE.function, + "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.BONEMEAL.function, + "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FILL_BUCKET.function, + "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.WAKE_UP.function, + "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.SLEEP.function, + "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PLAYSOUND.function, + "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.LIGHTNING.function, + "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.SCRIPT_COMMAND.function, + "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); + + // Quest events + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_START.function, + "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_COMPLETED.function, + "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_TURNIN.function, + "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); + + // Dialog events + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPEN.function, + "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPTION.function, + "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, + "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); + + // Faction events + registerBuiltInDefinition(ctx, EnumScriptType.FACTION_POINTS.function, + "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); + + // Custom GUI events + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, + "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, + "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, + "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, + "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, + "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, + "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); + + // Party events + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_SET.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_INVITE.function, + "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_KICK.function, + "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_LEAVE.function, + "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_DISBAND.function, + "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); + + // Animation events + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_START.function, + "noppes.npcs.api.event.IAnimationEvent$Started", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_END.function, + "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, + "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, + "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); + + // Profile events + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CHANGE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_REMOVE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CREATE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); + + // Effect events (for player context) + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_ADD.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_TICK.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); + } + + private void initializeBlockHooks() { + String ctx = ScriptContext.BLOCK.hookContext; + String blockImport = "noppes.npcs.api.event.IBlockEvent"; + + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.FALLEN_UPON.function, + "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.REDSTONE.function, + "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.BROKEN.function, + "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.EXPLODED.function, + "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.RAIN_FILLED.function, + "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, + "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.CLICKED.function, + "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.HARVESTED.function, + "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, + "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); + } + + private void initializeItemHooks() { + String ctx = ScriptContext.ITEM.hookContext; + String itemImport = "noppes.npcs.api.event.IItemEvent"; + + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, + "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, + "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, + "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + } + + private void initializeLinkedItemHooks() { + String ctx = ScriptContext.LINKED_ITEM.hookContext; + String itemImport = "noppes.npcs.api.event.IItemEvent"; + String linkedImport = "noppes.npcs.api.event.ILinkedItemEvent"; + + // Linked item specific hooks + registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, + "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); + registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, + "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); + + // Standard item hooks + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, + "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, + "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, + "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + } + + /** + * Register a built-in hook with full definition metadata. + */ + private void registerBuiltInDefinition(String context, String hookName, String eventClassName, String... imports) { + HookDefinition def = HookDefinition.builder(hookName) + .eventClass(eventClassName) + .requiredImports(imports) + .build(); + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new HashMap<>(); + hookDefinitions.put(context, contextDefs); + } + contextDefs.put(hookName, def); + + // Also register in legacy string list + registerBuiltIn(context, hookName); + } + /** * Register built-in hooks for a ScriptContext. */ @@ -229,6 +476,72 @@ public class ScriptHookController implements IScriptHookHandler { } } + // ==================== RICH HOOK REGISTRATION (NEW API) ==================== + + @Override + public void registerHookDefinition(String context, IHookDefinition definition) { + if (context == null || definition == null || definition.hookName() == null) { + return; + } + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new HashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + // Store as HookDefinition (implementation) + if (definition instanceof HookDefinition) { + contextDefs.put(definition.hookName(), (HookDefinition) definition); + } else { + // Convert foreign IHookDefinition to HookDefinition + HookDefinition def = HookDefinition.builder(definition.hookName()) + .eventClass(definition.eventClassName()) + .paramNames(definition.paramNames()) + .requiredImports(definition.requiredImports()) + .cancelable(definition.isCancelable()) + .build(); + contextDefs.put(definition.hookName(), def); + } + + // Also add to legacy addon hooks list for backward compatibility + registerHook(context, definition.hookName()); + + // Increment revision for cache invalidation + hookRevision++; + } + + @Override + public IHookDefinition getHookDefinition(String context, String hookName) { + if (context == null || hookName == null) { + return null; + } + + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null ? contextDefs.get(hookName) : null; + } + + @Override + public List getAllHookDefinitions(String context) { + if (context == null) { + return Collections.emptyList(); + } + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) { + return Collections.emptyList(); + } + + return new ArrayList<>(contextDefs.values()); + } + + @Override + public int getHookRevision() { + return hookRevision; + } + + // ==================== CONVENIENCE METHODS ==================== + /** * Convenience method to register a hook using ScriptContext. * @param context The script context @@ -248,7 +561,18 @@ public class ScriptHookController implements IScriptHookHandler { registerHook(context.hookContext, fn); } } - + + /** + * Convenience method to register a hook definition using ScriptContext. + * @param context The script context + * @param definition The hook definition + */ + public void registerHookDefinition(ScriptContext context, IHookDefinition definition) { + registerHookDefinition(context.hookContext, definition); + } + + // ==================== LEGACY HOOK REGISTRATION ==================== + @Override public void registerHook(String context, String functionName) { if (context == null || functionName == null || functionName.isEmpty()) { diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8734304d9..6f6d6e55b 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -6,9 +6,11 @@ import io.github.somehussar.janinoloader.api.script.IScriptClassBody; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.config.ConfigScript; 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; @@ -52,6 +54,10 @@ public abstract class JaninoScript implements IScriptUnit { private final String[] defaultImports; + // Hook definition cache for fast lookup (NEW) + private Map hookDefinitionCache; + private int lastHookRevision = -1; + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; @@ -71,6 +77,79 @@ public abstract class JaninoScript implements IScriptUnit { this(type, defaultImports, false); } + // ==================== HOOK CONTEXT (Abstract) ==================== + + /** + * Get the hook context identifier for this script type. + * Used to look up hook definitions from the registry. + * + * @return Context string, e.g., "npc", "player", "block" + */ + protected abstract String getHookContext(); + + // ==================== HOOK DEFINITION CACHE ==================== + + /** + * Get hook definition from cache, rebuilding cache if revision changed. + * + * @param hookName The hook name to look up + * @return The hook definition, or null if not found + */ + protected IHookDefinition getHookDefinition(String hookName) { + if (ScriptHookController.Instance == null) { + return null; + } + + int currentRevision = ScriptHookController.Instance.getHookRevision(); + if (hookDefinitionCache == null || currentRevision != lastHookRevision) { + rebuildHookDefinitionCache(); + lastHookRevision = currentRevision; + } + + return hookDefinitionCache.get(hookName); + } + + /** + * Rebuild the hook definition cache from the registry. + */ + private void rebuildHookDefinitionCache() { + hookDefinitionCache = new HashMap<>(); + String context = getHookContext(); + + if (context == null || context.isEmpty() || ScriptHookController.Instance == null) { + return; + } + + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + hookDefinitionCache.put(def.hookName(), def); + } + } + + /** + * Get all required imports from hook definitions plus default imports. + * + * @return Array of all import statements needed + */ + protected String[] collectAllImports() { + Set allImports = new LinkedHashSet<>(); + + // Add default imports first + Collections.addAll(allImports, defaultImports); + + // Add imports from all hook definitions for this context + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + String[] imports = def.requiredImports(); + if (imports != null) { + Collections.addAll(allImports, imports); + } + } + } + + return allImports.toArray(new String[0]); + } + protected T getUnsafe() { return scriptBody.get(); } @@ -119,11 +198,16 @@ public abstract class JaninoScript implements IScriptUnit { } /** - * Feed the code into the engine and compile it + * Feed the code into the engine and compile it. + * Rebuilds imports from hook definitions before compilation. */ public void compileScript(String code) { try { + // Rebuild imports from current hook definitions + defaults + String[] allImports = collectAllImports(); + builder.setDefaultImports(allImports); + scriptBody.setScript(code); } catch (InternalCompilerException e ) { Throwable parentCause = null; @@ -330,65 +414,43 @@ public abstract class JaninoScript implements IScriptUnit { return script != null && !script.isEmpty(); } - @Override - public String generateHookStub(String hookName, Object hookData) { - hookResolver.clearResolutionCaches(); + @Override + public String generateHookStub(String hookName, Object hookData) { + hookResolver.clearResolutionCaches(); - Method method = hookResolver.getMethodForDisplayName(hookName); - if (method != null) - return generateMethodStub(method); + // 1. Try interface method (existing @ScriptHook annotated methods) + Method method = hookResolver.getMethodForDisplayName(hookName); + if (method != null) + return generateMethodStub(method); - // Fallback: try to find the event class by naming convention - String eventType = findEventTypeForHook(hookName); - if (eventType != null) { - return String.format("public void %s(%s event) {\n \n}\n", hookName, eventType); + // 2. Look up in hook definition registry (addon hooks, dynamic hooks) + IHookDefinition def = getHookDefinition(hookName); + if (def != null) { + return generateStubFromDefinition(def); } - // Last resort: generic Object parameter + // 3. Last resort: generic Object parameter return String.format("public void %s(Object event) {\n \n}\n", hookName); } /** - * Tries to find the event type for a hook by searching API event interfaces. - * Uses naming convention: hookName "questStart" -> event class "QuestStartEvent" + * Generate a method stub from a hook definition. * - * @param hookName The hook name (e.g., "questStart", "dialogOpen") - * @return The usable type name (e.g., "IQuestEvent.QuestStartEvent") or null if not found + * @param def The hook definition containing type info + * @return Generated stub code */ - private String findEventTypeForHook(String hookName) { - if (hookName == null || hookName.isEmpty()) - return null; + private String generateStubFromDefinition(IHookDefinition def) { + String typeName = def.getUsableTypeName(); + String[] params = def.paramNames(); + String paramName = (params != null && params.length > 0) ? params[0] : "event"; - // Convert hook name to expected event class name - // questStart -> QuestStartEvent - String expectedEventName = Character.toUpperCase(hookName.charAt(0)) + hookName.substring(1) + "Event"; - - // Search in known API event interfaces - Class[] eventInterfaces = { - noppes.npcs.api.event.IPlayerEvent.class, - noppes.npcs.api.event.INpcEvent.class, - noppes.npcs.api.event.IQuestEvent.class, - noppes.npcs.api.event.IDialogEvent.class, - noppes.npcs.api.event.IFactionEvent.class, - noppes.npcs.api.event.IPartyEvent.class, - noppes.npcs.api.event.ICustomGuiEvent.class, - noppes.npcs.api.event.IAnimationEvent.class, - noppes.npcs.api.event.IBlockEvent.class, - noppes.npcs.api.event.IItemEvent.class, - noppes.npcs.api.event.IProjectileEvent.class, - noppes.npcs.api.event.IAbilityEvent.class, - noppes.npcs.api.event.IForgeEvent.class - }; - - for (Class eventInterface : eventInterfaces) { - for (Class nested : eventInterface.getDeclaredClasses()) { - if (nested.getSimpleName().equals(expectedEventName)) { - return getUsableTypeName(nested); - } - } + if (typeName == null || typeName.isEmpty()) { + return String.format("public void %s(Object %s) {\n \n}\n", + def.hookName(), paramName); } - return null; + return String.format("public void %s(%s %s) {\n \n}\n", + def.hookName(), typeName, paramName); } /** diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 1af8dea35..637374775 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -17,6 +17,7 @@ public class JaninoNpcScript extends JaninoScript { /** * 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", @@ -31,6 +32,11 @@ public class JaninoNpcScript extends JaninoScript { super(Functions.class, DEFAULT_IMPORTS); } + @Override + protected String getHookContext() { + return "npc"; + } + /** * Interface defining the hook methods available for NPC scripts. * Each method corresponds to an NPC event from ScriptHookController CONTEXT_NPC. diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index c755c700c..d29b1b15e 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -43,6 +43,7 @@ public class JaninoPlayerScript extends JaninoScript