From 78f7fa65f10e9dc8f5e85c96efa0ddba1667464d Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 9 Jan 2026 22:59:13 -0500 Subject: [PATCH] Add Script Context and better parsing for d.ts handling --- .../dts/JavaToTypeScriptConverter.groovy | 172 ++++++++++--- .../noppes/npcs/client/gui/GuiScript.java | 5 + .../client/gui/script/GuiScriptBlock.java | 6 + .../client/gui/script/GuiScriptInterface.java | 14 ++ .../npcs/client/gui/script/GuiScriptItem.java | 6 + .../client/gui/script/GuiScriptPlayers.java | 6 + .../client/gui/util/GuiScriptTextArea.java | 22 ++ .../script/interpreter/ScriptDocument.java | 30 ++- .../interpreter/ScriptTextContainer.java | 29 ++- .../interpreter/js_parser/JSTypeRegistry.java | 238 ++++++++++++++++-- .../js_parser/TypeScriptDefinitionParser.java | 69 +++-- .../script/interpreter/type/TypeResolver.java | 100 ++++++++ .../noppes/npcs/constants/ScriptContext.java | 201 +++++++++++++++ 13 files changed, 831 insertions(+), 67 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/ScriptContext.java diff --git a/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy b/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy index 5b73f36ad..16d71bc90 100644 --- a/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy +++ b/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy @@ -58,8 +58,10 @@ class JavaToTypeScriptConverter { private List generatedTypes = [] // Track hooks for hooks.d.ts + // Hooks are organized by their parent event type (e.g., INpcEvent, IPlayerEvent) + // The namespace in hooks.d.ts matches the event type name directly private Map> hooks = [:] - + private Logger logger; JavaToTypeScriptConverter(File outputDir, Set apiPackages) { @@ -1005,40 +1007,100 @@ class JavaToTypeScriptConverter { } /** - * Collect hook information from event interfaces + * Extract @hookName value from a JSDoc comment. + * + * @param jsdoc The JSDoc comment string (may be null) + * @return The hook name if @hookName tag is present, null otherwise + */ + private String extractHookNameFromJsDoc(String jsdoc) { + if (jsdoc == null || jsdoc.isEmpty()) return null + + // Match @hookName followed by the hook name value + // Examples: @hookName animationStart, @hookName customGuiButton + def matcher = jsdoc =~ /@hookName\s+(\w+)/ + if (matcher.find()) { + return matcher.group(1) + } + return null + } + + /** + * Collect hook information from event interfaces. + * * Looks for interfaces that: * 1. End with 'Event' (e.g., IPlayerEvent, IDBCEvent) * 2. Are in an 'event' package * 3. Have nested types that are also events + * + * Hook names are determined by: + * 1. @hookName JSDoc tag if present (e.g., @hookName animationStart) + * 2. Otherwise derived from the event class name (e.g., InitEvent -> init) + * + * The parent event type name (e.g., "INpcEvent", "IPlayerEvent") is used directly + * as the namespace in hooks.d.ts. This allows any mod to register event interfaces + * and have them automatically organized by their type name. */ private void collectHooks(ParsedJavaFile parsed) { boolean isEventPackage = parsed.packageName.contains('.event') - + parsed.types.each { type -> // Check if this is an event interface: // - Must be an interface // - Either ends with 'Event' OR is in an event package boolean isEventType = type.isInterface && ( - type.name.endsWith('Event') || type.name.endsWith('Events')|| + type.name.endsWith('Event') || type.name.endsWith('Events') || isEventPackage ) - + if (isEventType && !type.nestedTypes.isEmpty()) { - type.nestedTypes.each { nested -> - // Only process nested types that are interfaces and look like events - if (nested.isInterface && (nested.name.endsWith('Event')|| nested.name.endsWith('Events') || isEventPackage)) { - // Create hook entry - String hookName = deriveHookName(nested.name) - if (!hooks.containsKey(hookName)) { - hooks[hookName] = [] - } - hooks[hookName] << new HookInfo( - eventType: type.name, - subEvent: nested.name, - fullType: "${type.name}.${nested.name}", - packageName: parsed.packageName - ) - } + collectHooksFromNestedTypes(type.nestedTypes, type.name, parsed.packageName, isEventPackage) + } + } + } + + /** + * Recursively collect hooks from nested types. + * This handles deeply nested event types like IAnimationEvent.IFrameEvent.Entered + */ + private void collectHooksFromNestedTypes(List nestedTypes, String parentTypeName, String packageName, boolean isEventPackage) { + nestedTypes.each { nested -> + // Check if this nested type should be processed as a hook + boolean isNestedEvent = nested.isInterface && ( + nested.name.endsWith('Event') || + nested.name.endsWith('Events') || + isEventPackage || + // Also include simple names like "Started", "Ended", "Entered", "Exited" + // if they're inside an event interface + parentTypeName.endsWith('Event') + ) + + if (isNestedEvent) { + // Check for @hookName in JSDoc first, otherwise derive from class name + String hookName = extractHookNameFromJsDoc(nested.jsdoc) + if (hookName == null) { + hookName = deriveHookName(nested.name) + } + + if (!hooks.containsKey(hookName)) { + hooks[hookName] = [] + } + + // Use the root event type name as the namespace + // Extract the root type (e.g., "IAnimationEvent" from "IAnimationEvent.IFrameEvent") + String rootType = parentTypeName.contains('.') ? + parentTypeName.substring(0, parentTypeName.indexOf('.')) : parentTypeName + + hooks[hookName] << new HookInfo( + eventType: rootType, + subEvent: nested.name, + fullType: "${parentTypeName}.${nested.name}", + packageName: packageName, + contextNamespace: rootType + ) + + // Recursively process any nested types within this nested type + if (!nested.nestedTypes.isEmpty()) { + collectHooksFromNestedTypes(nested.nestedTypes, "${parentTypeName}.${nested.name}", packageName, isEventPackage) } } } @@ -1131,30 +1193,67 @@ class JavaToTypeScriptConverter { } /** - * Generate hooks.d.ts with event hook function declarations + * Generate hooks.d.ts with event hook function declarations. + * + * Hooks are organized by their parent event interface type, using the type name + * directly as the namespace. This provides context-aware autocomplete where + * the same hook name (e.g., "interact") can have different event types depending + * on the script context. + * + * Output format example: + * declare namespace INpcEvent { + * function interact(event: INpcEvent.InteractEvent): void; + * function init(event: INpcEvent.InitEvent): void; + * } + * + * declare namespace IPlayerEvent { + * function interact(event: IPlayerEvent.InteractEvent): void; + * function init(event: IPlayerEvent.InitEvent): void; + * } + * + * The namespace name matches the event interface type exactly, making it easy + * for the runtime parser to match hooks to their correct context. */ private void generateHooksFile() { StringBuilder sb = new StringBuilder() - + sb.append('/**\n') - sb.append(' * CustomNPC+ Event Hook Overloads\n') - sb.append(' * Auto-generated - do not edit manually.\n') + sb.append(' * CustomNPC+ Event Hook Declarations\n') + sb.append(' *\n') + sb.append(' * Hooks are organized by their parent event interface (e.g., INpcEvent, IPlayerEvent).\n') + sb.append(' * This allows the same hook name to have different event types per script context.\n') + sb.append(' *\n') + sb.append(' * Auto-generated from Java event interfaces - do not edit manually.\n') sb.append(' */\n\n') - + sb.append("import './minecraft-raw.d.ts';\n") sb.append("import './forge-events-raw.d.ts';\n\n") - - sb.append('declare global {\n') - + + // Group hooks by their parent event type (contextNamespace = type.name) + Map>> hooksByEventType = [:].withDefault { [:].withDefault { [] } } + hooks.each { hookName, hookInfos -> hookInfos.each { info -> - sb.append(" function ${hookName}(${info.eventType}: ${info.fullType}): void;\n") + hooksByEventType[info.contextNamespace][hookName] << info } } - - sb.append('}\n\n') + + // Output each event type as a namespace + hooksByEventType.sort { a, b -> a.key <=> b.key }.each { eventTypeName, eventHooks -> + sb.append("declare namespace ${eventTypeName} {\n") + + // Output all hooks for this event type, sorted alphabetically + eventHooks.sort { a, b -> a.key <=> b.key }.each { hookName, hookInfos -> + hookInfos.each { info -> + sb.append(" function ${hookName}(event: ${info.fullType}): void;\n") + } + } + + sb.append('}\n\n') + } + sb.append('export {};\n') - + new File(outputDir, 'hooks.d.ts').text = sb.toString() } @@ -1215,9 +1314,10 @@ class JavaToTypeScriptConverter { } static class HookInfo { - String eventType - String subEvent - String fullType - String packageName + String eventType // Parent event interface name (e.g., "INpcEvent") + String subEvent // Nested event type name (e.g., "InteractEvent") + String fullType // Full type path (e.g., "INpcEvent.InteractEvent") + String packageName // Java package (e.g., "noppes.npcs.api.event") + String contextNamespace // Namespace in hooks.d.ts (same as eventType, e.g., "INpcEvent") } } diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index e710eef23..bfe909dee 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -13,6 +13,7 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.script.GuiNPCEventScripts; import noppes.npcs.client.gui.util.*; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.DataScript; @@ -100,6 +101,10 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall activeArea.init(guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); } + // Set language and script context for proper syntax highlighting and autocomplete + activeArea.setLanguage(script.getLanguage()); + activeArea.setScriptContext(ScriptContext.NPC); + addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); addButton(new GuiNpcButton(100, guiLeft + 315, guiTop + 25, 50, 20, "gui.copy")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java index d5a5f3f86..ec3ac43ee 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.blocks.tiles.TileScripted; +import noppes.npcs.constants.ScriptContext; public class GuiScriptBlock extends GuiScriptInterface { @@ -33,6 +34,11 @@ public class GuiScriptBlock extends GuiScriptInterface { loaded = true; } + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.BLOCK; + } + public void save() { if (loaded) { super.save(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index ed1a54934..e875e0619 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -25,6 +25,7 @@ import noppes.npcs.client.gui.util.ITextChangeListener; import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; @@ -197,6 +198,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // Set the scripting language for proper syntax highlighting activeArea.setLanguage(this.handler.getLanguage()); + // Set the script context for context-aware hook autocomplete + activeArea.setScriptContext(getScriptContext()); + // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { if (e.isPress()) { @@ -513,6 +517,16 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.initGui(); } + /** + * Get the script context for this GUI. + * Override in subclasses to return the appropriate context. + * + * @return The script context (default: GLOBAL) + */ + protected ScriptContext getScriptContext() { + return ScriptContext.GLOBAL; + } + public void save() { if (loaded) this.setScript(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java index 23a29b90c..d855a363b 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java @@ -6,6 +6,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomItems; import noppes.npcs.NBTTags; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.scripted.item.ScriptCustomItem; import java.util.Date; @@ -48,6 +49,11 @@ public class GuiScriptItem extends GuiScriptInterface { } } + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.ITEM; + } + public void save() { if (loaded) { super.save(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java index 501e348d1..9bb3ebdfd 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java @@ -4,6 +4,7 @@ import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.NBTTags; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.EffectScript; @@ -114,6 +115,11 @@ public class GuiScriptPlayers extends GuiScriptInterface { } } + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.PLAYER; + } + public void save() { if (loaded) { super.save(); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 5ab759183..026086a4c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -22,6 +22,7 @@ import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; import noppes.npcs.client.gui.util.script.interpreter.hover.TokenHoverRenderer; import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteManager; import noppes.npcs.client.key.impl.ScriptEditorKeys; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; @@ -2629,6 +2630,27 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container != null ? this.container.getLanguage() : "ECMAScript"; } + /** + * Set the script context (NPC, PLAYER, BLOCK, ITEM, etc.). + * This determines which hooks and event types are available for autocomplete. + * + * @param context The script context + */ + public void setScriptContext(ScriptContext context) { + if (this.container != null) { + this.container.setScriptContext(context); + } + } + + /** + * Get the current script context. + * + * @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.) + */ + public ScriptContext getScriptContext() { + return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; + } + public void setListener(ITextChangeListener listener) { this.listener = listener; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java index a9f53957b..5ead47766 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -23,6 +23,7 @@ import noppes.npcs.client.gui.util.script.interpreter.type.*; import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticField; import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod; import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType; +import noppes.npcs.constants.ScriptContext; import java.util.*; import java.util.regex.Matcher; @@ -125,6 +126,10 @@ public class ScriptDocument { // Script language: "ECMAScript", "Groovy", etc. private String language = "ECMAScript"; + // Script context: NPC, PLAYER, BLOCK, ITEM, etc. + // Determines which hooks and event types are available + private ScriptContext scriptContext = ScriptContext.GLOBAL; + // Layout properties public int lineHeight = 13; public int totalHeight; @@ -170,6 +175,25 @@ public class ScriptDocument { return "ECMAScript".equalsIgnoreCase(language); } + /** + * Set the script context (NPC, PLAYER, BLOCK, ITEM, etc.). + * This determines which hooks and event types are available for autocomplete. + * + * @param context The script context + */ + public void setScriptContext(ScriptContext context) { + this.scriptContext = context != null ? context : ScriptContext.GLOBAL; + } + + /** + * Get the current script context. + * + * @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.) + */ + public ScriptContext getScriptContext() { + return scriptContext; + } + // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { @@ -686,11 +710,13 @@ public class ScriptDocument { JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start()); // For JS hooks, infer parameter types from registry + // Use the script context's namespaces (e.g., ["IPlayerEvent", "IAnimationEvent"]) for lookup List params = new ArrayList<>(); TypeInfo returnType = TypeInfo.fromPrimitive("void"); + List namespaces = scriptContext != null ? scriptContext.getNamespaces() : Collections.singletonList("Global"); - if (typeResolver.isJSHook(funcName)) { - List sigs = typeResolver.getJSHookSignatures(funcName); + if (typeResolver.isJSHook(namespaces, funcName)) { + List sigs = typeResolver.getJSHookSignatures(namespaces, funcName); if (!sigs.isEmpty()) { JSTypeRegistry.HookSignature sig = sigs.get(0); documentation = sig.doc; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java index b905ca412..4836c0834 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java @@ -4,6 +4,7 @@ import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.JavaTextContainer; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.constants.ScriptContext; import java.util.ArrayList; import java.util.List; @@ -31,10 +32,13 @@ public class ScriptTextContainer extends JavaTextContainer { public static boolean USE_NEW_INTERPRETER = true; private ScriptDocument document; - + /** The scripting language: "ECMAScript", "Groovy", etc. */ private String language = "ECMAScript"; + /** The script context: NPC, PLAYER, BLOCK, ITEM, etc. */ + private ScriptContext scriptContext = ScriptContext.GLOBAL; + public ScriptTextContainer(String text) { super(text); if (USE_NEW_INTERPRETER) { @@ -68,6 +72,28 @@ public class ScriptTextContainer extends JavaTextContainer { return language; } + /** + * Set the script context (NPC, PLAYER, BLOCK, ITEM, etc.). + * This determines which hooks and event types are available for autocomplete. + * + * @param context The script context + */ + public void setScriptContext(ScriptContext context) { + this.scriptContext = context != null ? context : ScriptContext.GLOBAL; + if (document != null) { + document.setScriptContext(this.scriptContext); + } + } + + /** + * Get the current script context. + * + * @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.) + */ + public ScriptContext getScriptContext() { + return scriptContext; + } + @Override public void init(int width, int height) { if (!USE_NEW_INTERPRETER) { @@ -103,6 +129,7 @@ public class ScriptTextContainer extends JavaTextContainer { } document = new ScriptDocument(this.text, this.language); + document.setScriptContext(this.scriptContext); init(width, height); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java index 5d4dfe15c..680172f99 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java @@ -27,10 +27,18 @@ public class JSTypeRegistry { // Type aliases (simple name -> full type name) private final Map typeAliases = new HashMap<>(); - // Hook function signatures: functionName -> list of (paramName, paramType) pairs - // Multiple entries for overloaded hooks + // Context-aware hook function signatures: namespace -> functionName -> list of signatures + // The namespace is the event interface name (e.g., "INpcEvent", "IPlayerEvent") + // This allows any mod to register hooks without modifying an enum + private final Map>> contextHooks = new LinkedHashMap<>(); + + // Legacy hook storage: functionName -> list of (paramName, paramType) pairs + // Kept for backward compatibility with code that doesn't use contexts private final Map> hooks = new LinkedHashMap<>(); + // Fallback namespace for hooks that don't match a specific context + private static final String GLOBAL_NAMESPACE = "Global"; + // Global object instances: name -> type (e.g., "API" -> "AbstractNpcAPI") // These are treated as instance objects, not static classes private final Map globalEngineObjects = new LinkedHashMap<>(); @@ -275,11 +283,23 @@ public class JSTypeRegistry { } /** - * Register a hook function signature. + * Register a hook function signature with a namespace. + * + * @param namespace The event interface namespace (e.g., "INpcEvent", "IPlayerEvent") + * @param functionName The hook function name (e.g., "interact", "damaged") + * @param paramName The parameter name (e.g., "event") + * @param paramType The parameter type (e.g., "INpcEvent.InteractEvent") */ - public void registerHook(String functionName, String paramName, String paramType) { - hooks.computeIfAbsent(functionName, k -> new ArrayList<>()) - .add(new HookSignature(paramName, paramType)); + public void registerHook(String namespace, String functionName, String paramName, String paramType) { + HookSignature sig = new HookSignature(paramName, paramType, null, namespace); + + // Add to context-specific map + contextHooks.computeIfAbsent(namespace, k -> new LinkedHashMap<>()) + .computeIfAbsent(functionName, k -> new ArrayList<>()) + .add(sig); + + // Also add to legacy hooks map for backward compatibility + hooks.computeIfAbsent(functionName, k -> new ArrayList<>()).add(sig); } /** @@ -369,7 +389,190 @@ public class JSTypeRegistry { } return null; } - + + // ==================== Context-Aware Hook Methods ==================== + + /** + * Get hook signatures for a specific script context. + * Falls back to GLOBAL context if not found in the specified context. + * + * @param namespace The event interface namespace (e.g., "INpcEvent", "IPlayerEvent") + * @param functionName The hook function name + * @return List of hook signatures, or empty list if not found + */ + public List getHookSignatures(String namespace, String functionName) { + // First try the specific namespace + Map> namespaceMap = contextHooks.get(namespace); + if (namespaceMap != null) { + List sigs = namespaceMap.get(functionName); + if (sigs != null && !sigs.isEmpty()) { + return sigs; + } + } + + // Fall back to GLOBAL namespace + if (!GLOBAL_NAMESPACE.equals(namespace)) { + Map> globalMap = contextHooks.get(GLOBAL_NAMESPACE); + if (globalMap != null) { + List sigs = globalMap.get(functionName); + if (sigs != null && !sigs.isEmpty()) { + return sigs; + } + } + } + + return Collections.emptyList(); + } + + /** + * Check if a function name is a known hook in a specific namespace. + * + * @param namespace The event interface namespace + * @param functionName The hook function name + * @return true if the hook exists in this namespace or GLOBAL + */ + public boolean isHook(String namespace, String functionName) { + return !getHookSignatures(namespace, functionName).isEmpty(); + } + + /** + * Get the parameter type for a hook function in a specific namespace. + * Falls back to GLOBAL namespace if not found. + * + * @param namespace The event interface namespace + * @param functionName The hook function name + * @return The parameter type, or null if not found + */ + public String getHookParameterType(String namespace, String functionName) { + List sigs = getHookSignatures(namespace, functionName); + if (!sigs.isEmpty()) { + return sigs.get(0).paramType; + } + return null; + } + + /** + * Get all hook names for a specific namespace. + * + * @param namespace The event interface namespace + * @return Set of hook function names available in this namespace + */ + public Set getHookNames(String namespace) { + Set names = new HashSet<>(); + + // Add hooks from the specific namespace + Map> namespaceMap = contextHooks.get(namespace); + if (namespaceMap != null) { + names.addAll(namespaceMap.keySet()); + } + + // Also add GLOBAL hooks + if (!GLOBAL_NAMESPACE.equals(namespace)) { + Map> globalMap = contextHooks.get(GLOBAL_NAMESPACE); + if (globalMap != null) { + names.addAll(globalMap.keySet()); + } + } + + return names; + } + + /** + * Get all hooks organized by namespace. + * + * @return Map of namespace -> hookName -> signatures + */ + public Map>> getAllContextHooks() { + return Collections.unmodifiableMap(contextHooks); + } + + // ==================== MULTI-NAMESPACE LOOKUP (for ScriptContext) ==================== + + /** + * Get hook signatures by searching through multiple namespaces. + * This is used when a ScriptContext has multiple event types (e.g., Player has + * IPlayerEvent, IAnimationEvent, IPartyEvent, etc.) + * + * @param namespaces List of namespaces to search (in priority order) + * @param functionName The hook function name + * @return List of hook signatures from the first matching namespace, or empty list + */ + public List getHookSignatures(List namespaces, String functionName) { + // Search through all namespaces in order + for (String namespace : namespaces) { + Map> namespaceMap = contextHooks.get(namespace); + if (namespaceMap != null) { + List sigs = namespaceMap.get(functionName); + if (sigs != null && !sigs.isEmpty()) { + return sigs; + } + } + } + + // Fall back to GLOBAL namespace + Map> globalMap = contextHooks.get(GLOBAL_NAMESPACE); + if (globalMap != null) { + List sigs = globalMap.get(functionName); + if (sigs != null && !sigs.isEmpty()) { + return sigs; + } + } + + return Collections.emptyList(); + } + + /** + * Check if a function name is a known hook in any of the given namespaces. + * + * @param namespaces List of namespaces to search + * @param functionName The hook function name + * @return true if the hook exists in any namespace or GLOBAL + */ + public boolean isHook(List namespaces, String functionName) { + return !getHookSignatures(namespaces, functionName).isEmpty(); + } + + /** + * Get the parameter type for a hook function, searching through multiple namespaces. + * + * @param namespaces List of namespaces to search + * @param functionName The hook function name + * @return The parameter type, or null if not found + */ + public String getHookParameterType(List namespaces, String functionName) { + List sigs = getHookSignatures(namespaces, functionName); + if (!sigs.isEmpty()) { + return sigs.get(0).paramType; + } + return null; + } + + /** + * Get all hook names available in any of the given namespaces. + * + * @param namespaces List of namespaces to search + * @return Set of hook function names available in these namespaces + */ + public Set getHookNames(List namespaces) { + Set names = new HashSet<>(); + + // Add hooks from all specified namespaces + for (String namespace : namespaces) { + Map> namespaceMap = contextHooks.get(namespace); + if (namespaceMap != null) { + names.addAll(namespaceMap.keySet()); + } + } + + // Also add GLOBAL hooks + Map> globalMap = contextHooks.get(GLOBAL_NAMESPACE); + if (globalMap != null) { + names.addAll(globalMap.keySet()); + } + + return names; + } + /** * Resolve all type parameters for all types. * Called after all .d.ts files are loaded (Phase 2). @@ -530,31 +733,38 @@ public class JSTypeRegistry { types.clear(); typeAliases.clear(); hooks.clear(); + contextHooks.clear(); globalEngineObjects.clear(); initialized = false; } - + /** - * Represents a hook function signature. + * Represents a hook function signature with its namespace. */ public static class HookSignature { public final String paramName; public final String paramType; public final String doc; - + public final String namespace; // The event interface namespace (e.g., "INpcEvent") + public HookSignature(String paramName, String paramType) { - this(paramName, paramType, null); + this(paramName, paramType, null, GLOBAL_NAMESPACE); } - + public HookSignature(String paramName, String paramType, String doc) { + this(paramName, paramType, doc, GLOBAL_NAMESPACE); + } + + public HookSignature(String paramName, String paramType, String doc, String namespace) { this.paramName = paramName; this.paramType = paramType; this.doc = doc; + this.namespace = namespace != null ? namespace : GLOBAL_NAMESPACE; } - + @Override public String toString() { - return paramName + ": " + paramType; + return paramName + ": " + paramType + " [" + namespace + "]"; } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeScriptDefinitionParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeScriptDefinitionParser.java index c507cf8f2..3e0d8b440 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeScriptDefinitionParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeScriptDefinitionParser.java @@ -44,9 +44,15 @@ public class TypeScriptDefinitionParser { private static final Pattern GLOBAL_FUNCTION_PATTERN = Pattern.compile( "function\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*([^;]+);"); - + private static final Pattern GLOBAL_TYPE_ALIAS_PATTERN = Pattern.compile( "type\\s+(\\w+)\\s*=\\s*import\\(['\"]([^'\"]+)['\"]\\)\\.([\\w.]+);"); + + // Pattern for context-namespaced hooks like: declare namespace INpcEvent { ... } + // Matches any "declare namespace {" where Name starts with I and contains Event, + // or any other namespace pattern used for hooks + private static final Pattern HOOKS_NAMESPACE_PATTERN = Pattern.compile( + "declare\\s+namespace\\s+(I\\w*Event|\\w+)\\s*\\{", Pattern.MULTILINE); private final JSTypeRegistry registry; @@ -129,21 +135,56 @@ public class TypeScriptDefinitionParser { /** * Parse hooks.d.ts to extract function signatures for JS hooks. + * + * Hooks are organized by their parent event interface, with the interface name + * used directly as the namespace. Any mod can register its own event interfaces + * and they will be automatically parsed: + * + * declare namespace INpcEvent { + * function interact(event: INpcEvent.InteractEvent): void; + * function init(event: INpcEvent.InitEvent): void; + * } + * + * declare namespace IPlayerEvent { + * function interact(event: IPlayerEvent.InteractEvent): void; + * } + * + * declare namespace IDBCEvent { + * function customHook(event: IDBCEvent.CustomEvent): void; + * } + * + * The namespace name is stored as a string, allowing dynamic registration + * without requiring enum modifications. */ private void parseHooksFile(String content) { - Matcher m = GLOBAL_FUNCTION_PATTERN.matcher(content); - while (m.find()) { - String funcName = m.group(1); - String params = m.group(2); - String returnType = m.group(3).trim(); - - // Parse parameter - format is "paramName: TypeName" - if (!params.isEmpty()) { - String[] parts = params.split(":\\s*", 2); - if (parts.length == 2) { - String paramName = parts[0].trim(); - String paramType = parts[1].trim(); - registry.registerHook(funcName, paramName, paramType); + // Parse namespaced hooks (e.g., declare namespace INpcEvent { ... }) + // The namespace name is used directly - any event interface can register hooks + Matcher namespaceMatcher = HOOKS_NAMESPACE_PATTERN.matcher(content); + while (namespaceMatcher.find()) { + String namespace = namespaceMatcher.group(1); // e.g., "INpcEvent", "IPlayerEvent", "IDBCEvent" + + // Find the body of this namespace + int bodyStart = namespaceMatcher.end(); + int bodyEnd = findMatchingBrace(content, bodyStart - 1); + if (bodyEnd > bodyStart) { + String namespaceBody = content.substring(bodyStart, bodyEnd); + + // Parse functions within this namespace + Matcher funcMatcher = GLOBAL_FUNCTION_PATTERN.matcher(namespaceBody); + while (funcMatcher.find()) { + String funcName = funcMatcher.group(1); + String params = funcMatcher.group(2); + + // Parse parameter - format is "paramName: TypeName" + if (!params.isEmpty()) { + String[] parts = params.split(":\\s*", 2); + if (parts.length == 2) { + String paramName = parts[0].trim(); + String paramType = parts[1].trim(); + // Register hook with the namespace string directly + registry.registerHook(namespace, funcName, paramName, paramType); + } + } } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java index 2ef61c838..53aae5b06 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java @@ -166,6 +166,106 @@ public class TypeResolver { return getJSTypeRegistry().getHookSignatures(functionName); } + // ==================== CONTEXT-AWARE HOOK METHODS ==================== + + /** + * Check if a function name is a known JS hook in a specific script context. + * Falls back to GLOBAL context if not found in the specified context. + * + * @param namespace The event interface namespace (e.g., "INpcEvent", "IPlayerEvent") + * @param functionName The function name to check + * @return true if it's a known hook in this namespace + */ + public boolean isJSHook(String namespace, String functionName) { + return getJSTypeRegistry().isHook(namespace, functionName); + } + + /** + * Get the parameter type for a JS hook function in a specific namespace. + * Falls back to "Global" namespace if not found in the specified namespace. + * + * @param namespace The event interface namespace (e.g., "INpcEvent", "IPlayerEvent") + * @param functionName The hook function name + * @return The parameter type, or null if not a hook + */ + public String getJSHookParameterType(String namespace, String functionName) { + return getJSTypeRegistry().getHookParameterType(namespace, functionName); + } + + /** + * Get hook signatures for a JS function in a specific namespace. + * Falls back to "Global" namespace if not found in the specified namespace. + * + * @param namespace The event interface namespace (e.g., "INpcEvent", "IPlayerEvent") + * @param functionName The hook function name + * @return List of hook signatures + */ + public List getJSHookSignatures(String namespace, String functionName) { + return getJSTypeRegistry().getHookSignatures(namespace, functionName); + } + + /** + * Get all hook names available in a specific namespace. + * + * @param namespace The event interface namespace + * @return Set of hook function names + */ + public Set getJSHookNames(String namespace) { + return getJSTypeRegistry().getHookNames(namespace); + } + + // ==================== MULTI-NAMESPACE HOOK METHODS ==================== + + /** + * Check if a function name is a known JS hook in any of the provided namespaces. + * Checks namespaces in order, returning true on first match. + * Falls back to GLOBAL namespace if not found in any specified namespace. + * + * @param namespaces The list of event interface namespaces to search (e.g., ["IPlayerEvent", "IAnimationEvent"]) + * @param functionName The function name to check + * @return true if it's a known hook in any of the namespaces + */ + public boolean isJSHook(List namespaces, String functionName) { + return getJSTypeRegistry().isHook(namespaces, functionName); + } + + /** + * Get the parameter type for a JS hook function, searching multiple namespaces. + * Checks namespaces in order, returning the first match found. + * Falls back to "Global" namespace if not found in any specified namespace. + * + * @param namespaces The list of event interface namespaces to search + * @param functionName The hook function name + * @return The parameter type, or null if not a hook + */ + public String getJSHookParameterType(List namespaces, String functionName) { + return getJSTypeRegistry().getHookParameterType(namespaces, functionName); + } + + /** + * Get hook signatures for a JS function, searching multiple namespaces. + * Checks namespaces in order, returning signatures from the first matching namespace. + * Falls back to "Global" namespace if not found in any specified namespace. + * + * @param namespaces The list of event interface namespaces to search + * @param functionName The hook function name + * @return List of hook signatures + */ + public List getJSHookSignatures(List namespaces, String functionName) { + return getJSTypeRegistry().getHookSignatures(namespaces, functionName); + } + + /** + * Get all hook names available in any of the provided namespaces. + * Combines hook names from all specified namespaces. + * + * @param namespaces The list of event interface namespaces + * @return Combined set of hook function names from all namespaces + */ + public Set getJSHookNames(List namespaces) { + return getJSTypeRegistry().getHookNames(namespaces); + } + // ==================== CACHE MANAGEMENT ==================== /** diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java new file mode 100644 index 000000000..a4567955b --- /dev/null +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -0,0 +1,201 @@ +package noppes.npcs.constants; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Represents a script context that maps to one or more event interface namespaces. + * + * A script context can have multiple namespaces because script editors often + * support hooks from multiple event types. For example, Player scripts support: + * - IPlayerEvent (core player hooks) + * - IAnimationEvent (animation hooks) + * - IPartyEvent (party hooks) + * - ICustomGuiEvent (custom GUI hooks) + * - etc. + * + * This is a registerable system - any mod can register their own contexts: + * + * // Register a new context with multiple namespaces: + * ScriptContext.register("DBC", "IDBCEvent", "IDBCPlayerEvent", "IDBCFormEvent"); + * + * // Or add namespaces to an existing context: + * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); + */ +public class ScriptContext { + + // Registry of all script contexts + private static final Map REGISTRY = new ConcurrentHashMap<>(); + + // ==================== BUILT-IN CONTEXTS ==================== + + public static final ScriptContext NPC = register("NPC", + "INpcEvent", + "IProjectileEvent" + ); + + public static final ScriptContext PLAYER = register("PLAYER", + "IPlayerEvent", + "IAnimationEvent", + "IPartyEvent", + "IDialogEvent", + "IQuestEvent", + "IFactionEvent", + "ICustomGuiEvent", + "IEffectEvent" + ); + + public static final ScriptContext BLOCK = register("BLOCK", + "IBlockEvent" + ); + + public static final ScriptContext ITEM = register("ITEM", + "IItemEvent" + ); + + public static final ScriptContext FORGE = register("FORGE", + "IForgeEvent" + ); + + public static final ScriptContext GLOBAL = register("GLOBAL", + "Global" + ); + + // ==================== INSTANCE FIELDS ==================== + + /** Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") */ + public final String id; + + /** The event interface namespaces this context supports */ + private final List namespaces; + + // ==================== CONSTRUCTOR ==================== + + private ScriptContext(String id, String... namespaces) { + this.id = id; + this.namespaces = new ArrayList<>(Arrays.asList(namespaces)); + } + + // ==================== NAMESPACE ACCESS ==================== + + /** + * Get all namespaces this context supports. + * + * @return Unmodifiable list of namespace strings + */ + public List getNamespaces() { + return Collections.unmodifiableList(namespaces); + } + + /** + * Check if this context includes a specific namespace. + * + * @param namespace The namespace to check (e.g., "IPlayerEvent") + * @return true if this context supports hooks from that namespace + */ + public boolean hasNamespace(String namespace) { + return namespaces.contains(namespace); + } + + /** + * Add a namespace to this context. + * Mods can use this to extend built-in contexts with their own event types. + * + * @param namespace The namespace to add (e.g., "IDBCPlayerEvent") + */ + public void addNamespace(String namespace) { + if (!namespaces.contains(namespace)) { + namespaces.add(namespace); + } + } + + /** + * Get the primary namespace (first one registered). + * This is mainly for backward compatibility. + * + * @return The first namespace, or "Global" if none + */ + public String getPrimaryNamespace() { + return namespaces.isEmpty() ? "Global" : namespaces.get(0); + } + + // ==================== REGISTRATION API ==================== + + /** + * Register a new script context with one or more namespaces. + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, String... namespaces) { + ScriptContext context = new ScriptContext(id, namespaces); + REGISTRY.put(id, context); + return context; + } + + /** + * Get a script context by its ID. + * + * @param id The context ID (e.g., "NPC", "PLAYER", "DBC") + * @return The ScriptContext, or GLOBAL if not found + */ + public static ScriptContext byId(String id) { + return REGISTRY.getOrDefault(id, GLOBAL); + } + + /** + * Find a script context that contains a specific namespace. + * + * @param namespace The event interface namespace (e.g., "INpcEvent") + * @return The first ScriptContext that contains this namespace, or GLOBAL if not found + */ + public static ScriptContext byNamespace(String namespace) { + if (namespace == null) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (ctx.hasNamespace(namespace)) { + return ctx; + } + } + return GLOBAL; + } + + /** + * Get all registered script contexts. + * + * @return Unmodifiable collection of all registered contexts + */ + public static Collection values() { + return Collections.unmodifiableCollection(REGISTRY.values()); + } + + /** + * Check if a context with the given ID exists. + * + * @param id The context ID to check + * @return true if registered + */ + public static boolean exists(String id) { + return REGISTRY.containsKey(id); + } + + // ==================== OBJECT METHODS ==================== + + @Override + public String toString() { + return id + " -> " + namespaces; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ScriptContext)) return false; + ScriptContext other = (ScriptContext) obj; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +}