From 51993c3efc91508b216f8ec645245f041ea4fa20 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 23 Dec 2025 00:01:23 +0200 Subject: [PATCH 001/337] HUGE MASSIVE REFACTOR! BRAND NEW SYNTAX HIGHLIGHT/IMPORT RESOLUTION FRAMEWORK --- .../client/gui/util/GuiScriptTextArea.java | 12 +- .../util/script/interpreter/FieldInfo.java | 111 +++ .../util/script/interpreter/ImportData.java | 115 +++ .../util/script/interpreter/MethodInfo.java | 145 +++ .../script/interpreter/ScriptDocument.java | 885 ++++++++++++++++++ .../util/script/interpreter/ScriptLine.java | 303 ++++++ .../interpreter/ScriptTextContainer.java | 199 ++++ .../gui/util/script/interpreter/Token.java | 237 +++++ .../util/script/interpreter/TokenType.java | 103 ++ .../gui/util/script/interpreter/TypeInfo.java | 173 ++++ .../util/script/interpreter/TypeResolver.java | 389 ++++++++ .../util/script/interpreter/package-info.java | 102 ++ 12 files changed, 2771 insertions(+), 3 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java 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 3426bcae8..bb77cfaad 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -10,6 +10,9 @@ import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +// New interpreter system imports +import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; +import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; import noppes.npcs.client.key.impl.ScriptEditorKeys; import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; @@ -54,7 +57,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== TEXT & CONTAINER ==================== public String text = null; public String highlightedWord; - private JavaTextContainer container = null; + private ScriptTextContainer container = null; private boolean enableCodeHighlighting = false; // Extra empty lines to allow padding at the bottom of the editor viewport private int bottomPaddingLines = 6; @@ -643,6 +646,7 @@ public void drawTextBox(int xMouse, int yMouse) { // Render Viewport for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); + ScriptLine scriptLine = container.getDocument().getLine(i); String line = data.text; int w = line.length(); // Use integer Y relative to scrolledLine; fractional offset applied via GL translate @@ -832,7 +836,9 @@ public void drawTextBox(int xMouse, int yMouse) { } } int yPos = posY + stringYOffset; - data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + + // data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + scriptLine.drawString(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); // Draw cursor: pause blinking while user is active recently boolean recentInput = selection.hadRecentInput(); @@ -2122,7 +2128,7 @@ public void setText(String text) { this.text = text; //this.container = new TextContainer(text); if (this.container == null) - this.container = new JavaTextContainer(text); + this.container = new ScriptTextContainer(text); this.container.init(text, this.width, this.height); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java new file mode 100644 index 000000000..8d4e85e09 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -0,0 +1,111 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Metadata for a field (variable) declaration or reference. + * Tracks the field name, type, scope, and declaration location. + */ +public final class FieldInfo { + + public enum Scope { + GLOBAL, // Class-level field + LOCAL, // Local variable inside a method + PARAMETER // Method parameter + } + + private final String name; + private final Scope scope; + private final TypeInfo declaredType; // The declared type (e.g., String, List) + private final int declarationOffset; // Where this field was declared in the source + private final boolean resolved; + + // For local/parameter fields, track the containing method + private final MethodInfo containingMethod; + + private FieldInfo(String name, Scope scope, TypeInfo declaredType, + int declarationOffset, boolean resolved, MethodInfo containingMethod) { + this.name = name; + this.scope = scope; + this.declaredType = declaredType; + this.declarationOffset = declarationOffset; + this.resolved = resolved; + this.containingMethod = containingMethod; + } + + // Factory methods + public static FieldInfo globalField(String name, TypeInfo type, int declOffset) { + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null); + } + + public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) { + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method); + } + + public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) { + return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method); + } + + public static FieldInfo unresolved(String name, Scope scope) { + return new FieldInfo(name, scope, null, -1, false, null); + } + + // Getters + public String getName() { return name; } + public Scope getScope() { return scope; } + public TypeInfo getDeclaredType() { return declaredType; } + public int getDeclarationOffset() { return declarationOffset; } + public boolean isResolved() { return resolved; } + public MethodInfo getContainingMethod() { return containingMethod; } + + public boolean isGlobal() { return scope == Scope.GLOBAL; } + public boolean isLocal() { return scope == Scope.LOCAL; } + public boolean isParameter() { return scope == Scope.PARAMETER; } + + /** + * Check if a reference at the given position can see this field. + * For local variables, they're only visible after their declaration. + */ + public boolean isVisibleAt(int position) { + if (scope == Scope.GLOBAL || scope == Scope.PARAMETER) { + return true; // Always visible in their scope + } + // Local variables are only visible after declaration + return position >= declarationOffset; + } + + /** + * Get the appropriate TokenType for highlighting this field. + */ + public TokenType getTokenType() { + if (!resolved) { + return TokenType.UNDEFINED_VAR; + } + switch (scope) { + case GLOBAL: + return TokenType.GLOBAL_FIELD; + case LOCAL: + return TokenType.LOCAL_FIELD; + case PARAMETER: + return TokenType.PARAMETER; + default: + return TokenType.VARIABLE; + } + } + + @Override + public String toString() { + return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldInfo fieldInfo = (FieldInfo) o; + return name.equals(fieldInfo.name) && scope == fieldInfo.scope; + } + + @Override + public int hashCode() { + return name.hashCode() * 31 + scope.ordinal(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java new file mode 100644 index 000000000..00557fa6a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java @@ -0,0 +1,115 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Represents a single import statement with all its metadata. + * Tracks the full import path, resolution status, and source positions. + */ +public final class ImportData { + + private final String fullPath; // e.g., "java.util.List" or "kamkeel.npcdbc.api.*" + private final String simpleName; // e.g., "List" or null for wildcard + private final boolean isWildcard; // true if ends with .* + private final boolean isStatic; // true if 'import static' + private final int startOffset; // start of 'import' keyword in source + private final int endOffset; // end of import statement (before or at semicolon) + private final int pathStartOffset; // start of the package.Class path + private final int pathEndOffset; // end of the package.Class path (before .*) + + private TypeInfo resolvedType; // null if wildcard or unresolved + private boolean resolved; // whether resolution was attempted and succeeded + + public ImportData(String fullPath, String simpleName, boolean isWildcard, boolean isStatic, + int startOffset, int endOffset, int pathStartOffset, int pathEndOffset) { + this.fullPath = fullPath; + this.simpleName = simpleName; + this.isWildcard = isWildcard; + this.isStatic = isStatic; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.pathStartOffset = pathStartOffset; + this.pathEndOffset = pathEndOffset; + this.resolved = false; + } + + // Getters + public String getFullPath() { return fullPath; } + public String getSimpleName() { return simpleName; } + public boolean isWildcard() { return isWildcard; } + public boolean isStatic() { return isStatic; } + public int getStartOffset() { return startOffset; } + public int getEndOffset() { return endOffset; } + public int getPathStartOffset() { return pathStartOffset; } + public int getPathEndOffset() { return pathEndOffset; } + public TypeInfo getResolvedType() { return resolvedType; } + public boolean isResolved() { return resolved; } + + /** + * Set the resolved type information. + */ + public void setResolvedType(TypeInfo typeInfo) { + this.resolvedType = typeInfo; + this.resolved = typeInfo != null && typeInfo.isResolved(); + } + + /** + * Mark this import as resolved (for wildcard imports where we confirm the package exists). + */ + public void markResolved(boolean resolved) { + this.resolved = resolved; + } + + /** + * Get the package portion of the import path. + * For "java.util.List" returns "java.util" + * For wildcards like "java.util.*" returns "java.util" + */ + public String getPackagePortion() { + if (isWildcard) { + return fullPath; + } + int lastDot = fullPath.lastIndexOf('.'); + return lastDot > 0 ? fullPath.substring(0, lastDot) : ""; + } + + /** + * Get all segments of the import path. + */ + public String[] getPathSegments() { + return fullPath.split("\\."); + } + + /** + * Check if this import could provide the given simple class name. + * For wildcard imports, always returns true (might provide any class). + * For specific imports, checks if the simple name matches. + */ + public boolean couldProvide(String className) { + if (isWildcard) { + return true; + } + return className.equals(simpleName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("import "); + if (isStatic) sb.append("static "); + sb.append(fullPath); + if (isWildcard) sb.append(".*"); + sb.append(" [").append(resolved ? "resolved" : "unresolved").append("]"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ImportData that = (ImportData) o; + return fullPath.equals(that.fullPath) && isWildcard == that.isWildcard && isStatic == that.isStatic; + } + + @Override + public int hashCode() { + return fullPath.hashCode() * 31 + (isWildcard ? 1 : 0) + (isStatic ? 2 : 0); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java new file mode 100644 index 000000000..95b9b76a5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -0,0 +1,145 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Metadata for a method declaration or method call. + * Tracks method name, parameters, return type, and containing class. + */ +public final class MethodInfo { + + private final String name; + private final TypeInfo returnType; + private final TypeInfo containingType; // The class/interface that owns this method + private final List parameters; + private final int declarationOffset; // -1 for external methods (calls to library methods) + private final int bodyStart; // Start of method body (after {) + private final int bodyEnd; // End of method body (before }) + private final boolean resolved; + private final boolean isDeclaration; // true if this is a declaration, false if it's a call + + private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, + List parameters, int declarationOffset, + int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration) { + this.name = name; + this.returnType = returnType; + this.containingType = containingType; + this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>(); + this.declarationOffset = declarationOffset; + this.bodyStart = bodyStart; + this.bodyEnd = bodyEnd; + this.resolved = resolved; + this.isDeclaration = isDeclaration; + } + + // Factory methods + public static MethodInfo declaration(String name, TypeInfo returnType, List params, + int declOffset, int bodyStart, int bodyEnd) { + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true); + } + + public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { + boolean resolved = containingType != null && containingType.isResolved() && + containingType.hasMethod(name); + List params = new ArrayList<>(); + for (int i = 0; i < paramCount; i++) { + params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); + } + return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false); + } + + public static MethodInfo unresolvedCall(String name, int paramCount) { + List params = new ArrayList<>(); + for (int i = 0; i < paramCount; i++) { + params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); + } + return new MethodInfo(name, null, null, params, -1, -1, -1, false, false); + } + + // Getters + public String getName() { return name; } + public TypeInfo getReturnType() { return returnType; } + public TypeInfo getContainingType() { return containingType; } + public List getParameters() { return Collections.unmodifiableList(parameters); } + public int getParameterCount() { return parameters.size(); } + public int getDeclarationOffset() { return declarationOffset; } + public int getBodyStart() { return bodyStart; } + public int getBodyEnd() { return bodyEnd; } + public boolean isResolved() { return resolved; } + public boolean isDeclaration() { return isDeclaration; } + public boolean isCall() { return !isDeclaration; } + + /** + * Check if a position is inside this method's body. + */ + public boolean containsPosition(int position) { + return position >= bodyStart && position < bodyEnd; + } + + /** + * Check if this method has a parameter with the given name. + */ + public boolean hasParameter(String paramName) { + for (FieldInfo p : parameters) { + if (p.getName().equals(paramName)) { + return true; + } + } + return false; + } + + /** + * Get parameter info by name. + */ + public FieldInfo getParameter(String paramName) { + for (FieldInfo p : parameters) { + if (p.getName().equals(paramName)) { + return p; + } + } + return null; + } + + /** + * Get the appropriate TokenType for highlighting this method. + */ + public TokenType getTokenType() { + if (isDeclaration) { + return TokenType.METHOD_DECL; + } + return resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MethodInfo{"); + sb.append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(parameters.get(i).getName()); + } + sb.append(")"); + if (returnType != null) { + sb.append(" -> ").append(returnType.getSimpleName()); + } + sb.append(", ").append(isDeclaration ? "decl" : "call"); + sb.append(", ").append(resolved ? "resolved" : "unresolved"); + sb.append("}"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MethodInfo that = (MethodInfo) o; + return name.equals(that.name) && parameters.size() == that.parameters.size(); + } + + @Override + public int hashCode() { + return name.hashCode() * 31 + parameters.size(); + } +} 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 new file mode 100644 index 000000000..33399c954 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptDocument.java @@ -0,0 +1,885 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.ClientProxy; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The main document container that manages script text, lines, and tokens. + * This is the clean reimplementation of JavaTextContainer. + * + * Architecture: + * - ScriptDocument holds the complete source text and global state + * - ScriptLine holds individual lines with their tokens + * - Token holds individual syntax elements with type-specific metadata + * - TypeResolver handles all class/type resolution + * + * Single-pass tokenization: + * 1. Parse excluded regions (comments, strings) + * 2. Parse imports and resolve types + * 3. Parse structure (methods, classes, fields) + * 4. Tokenize with all context available + */ +public class ScriptDocument { + + // ==================== PATTERNS ==================== + + private static final Pattern WORD_PATTERN = Pattern.compile("[\\p{L}\\p{N}_-]+|\\n|$"); + + // Literals + private static final Pattern STRING_PATTERN = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); + private static final Pattern COMMENT_PATTERN = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); + private static final Pattern NUMBER_PATTERN = Pattern.compile( + "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); + + // Keywords + private static final Pattern MODIFIER_PATTERN = Pattern.compile( + "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); + private static final Pattern KEYWORD_PATTERN = Pattern.compile( + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); + + // Declarations + private static final Pattern IMPORT_PATTERN = Pattern.compile( + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); + private static final Pattern CLASS_DECL_PATTERN = Pattern.compile( + "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + private static final Pattern METHOD_DECL_PATTERN = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + private static final Pattern METHOD_CALL_PATTERN = Pattern.compile( + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + private static final Pattern FIELD_DECL_PATTERN = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + + // Function parameters (for JS-style scripts) + private static final Pattern FUNC_PARAMS_PATTERN = Pattern.compile( + "\\bfunction\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*\\(([^)]*)\\)"); + + // ==================== STATE ==================== + + private String text = ""; + private final List lines = new ArrayList<>(); + private final List imports = new ArrayList<>(); + private final List methods = new ArrayList<>(); + private final Map globalFields = new HashMap<>(); + private final Set wildcardPackages = new HashSet<>(); + private Map importsBySimpleName = new HashMap<>(); + + // Excluded regions (strings/comments) - positions where other patterns shouldn't match + private final List excludedRanges = new ArrayList<>(); + + // Type resolver + private final TypeResolver typeResolver; + + // Layout properties + public int lineHeight = 13; + public int totalHeight; + public int visibleLines = 1; + public int linesCount; + + // ==================== CONSTRUCTOR ==================== + + public ScriptDocument(String text) { + this.typeResolver = TypeResolver.getInstance(); + setText(text); + } + + public ScriptDocument(String text, TypeResolver resolver) { + this.typeResolver = resolver != null ? resolver : TypeResolver.getInstance(); + setText(text); + } + + // ==================== TEXT MANAGEMENT ==================== + + public void setText(String text) { + this.text = text != null ? text.replaceAll("\\r?\\n|\\r", "\n") : ""; + } + + public String getText() { + return text; + } + + // ==================== INITIALIZATION ==================== + + /** + * Initialize the document with layout constraints. + * Builds lines based on width wrapping. + */ + public void init(int width, int height) { + lines.clear(); + lineHeight = ClientProxy.Font.height(); + if (lineHeight == 0) + lineHeight = 12; + + String[] sourceLines = text.split("\n", -1); + int totalChars = 0; + int lineIndex = 0; + + for (String sourceLine : sourceLines) { + StringBuilder currentLine = new StringBuilder(); + Matcher m = WORD_PATTERN.matcher(sourceLine); + int i = 0; + + while (m.find()) { + String word = sourceLine.substring(i, m.start()); + if (ClientProxy.Font.width(currentLine + word) > width - 10) { + // Wrap line + int lineStart = totalChars; + int lineEnd = totalChars + currentLine.length(); + lines.add(new ScriptLine(currentLine.toString(), lineStart, lineEnd, lineIndex++)); + totalChars = lineEnd; + currentLine = new StringBuilder(); + } + currentLine.append(word); + i = m.start(); + } + + // Add final line segment (including newline character in range) + int lineStart = totalChars; + int lineEnd = totalChars + currentLine.length() + 1; + lines.add(new ScriptLine(currentLine.toString(), lineStart, lineEnd, lineIndex++)); + totalChars = lineEnd; + } + + // Set up line navigation + for (int li = 0; li < lines.size(); li++) { + ScriptLine line = lines.get(li); + line.setParent(this); + if (li > 0) { + line.setPrev(lines.get(li - 1)); + lines.get(li - 1).setNext(line); + } + } + + linesCount = lines.size(); + totalHeight = linesCount * lineHeight; + visibleLines = Math.max(height / lineHeight - 1, 1); + } + + // ==================== TOKENIZATION ==================== + + /** + * Main tokenization entry point. + * Performs complete analysis and builds tokens for all lines. + */ + public void formatCodeText() { + // Clear previous state + imports.clear(); + methods.clear(); + globalFields.clear(); + wildcardPackages.clear(); + excludedRanges.clear(); + + // Phase 1: Find excluded regions (strings/comments) + findExcludedRanges(); + + // Phase 2: Parse imports + parseImports(); + + // Phase 3: Parse structure (methods, fields) + parseStructure(); + + // Phase 4: Build marks and assign to lines + List marks = buildMarks(); + + // Phase 5: Resolve conflicts and sort + marks = resolveConflicts(marks); + + // Phase 6: Build tokens for each line + for (ScriptLine line : lines) { + line.buildTokensFromMarks(marks, text); + } + + // Phase 7: Compute indent guides + computeIndentGuides(marks); + } + + // ==================== PHASE 1: EXCLUDED RANGES ==================== + + private void findExcludedRanges() { + // Find strings + Matcher m = STRING_PATTERN.matcher(text); + while (m.find()) { + excludedRanges.add(new int[]{m.start(), m.end()}); + } + + // Find comments + m = COMMENT_PATTERN.matcher(text); + while (m.find()) { + excludedRanges.add(new int[]{m.start(), m.end()}); + } + + // Sort and merge overlapping ranges + excludedRanges.sort(Comparator.comparingInt(a -> a[0])); + mergeOverlappingRanges(); + } + + private void mergeOverlappingRanges() { + if (excludedRanges.size() < 2) + return; + + List merged = new ArrayList<>(); + int[] current = excludedRanges.get(0); + + for (int i = 1; i < excludedRanges.size(); i++) { + int[] next = excludedRanges.get(i); + if (next[0] <= current[1]) { + current[1] = Math.max(current[1], next[1]); + } else { + merged.add(current); + current = next; + } + } + merged.add(current); + + excludedRanges.clear(); + excludedRanges.addAll(merged); + } + + private boolean isExcluded(int position) { + for (int[] range : excludedRanges) { + if (position >= range[0] && position < range[1]) { + return true; + } + } + return false; + } + + // ==================== PHASE 2: IMPORTS ==================== + + private void parseImports() { + Matcher m = IMPORT_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String fullPath = m.group(1).replaceAll("\\s+", "").trim(); + String matchText = m.group(0); + boolean isWildcard = matchText.contains("*"); + boolean isStatic = matchText.contains("static"); + + int lastDot = fullPath.lastIndexOf('.'); + String simpleName = isWildcard ? null : (lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath); + + ImportData importData = new ImportData( + fullPath, simpleName, isWildcard, isStatic, + m.start(), m.end(), m.start(1), m.end(1) + ); + imports.add(importData); + + if (isWildcard) { + wildcardPackages.add(fullPath); + } + } + + // Resolve all imports + importsBySimpleName = typeResolver.resolveImports(imports); + } + + // ==================== PHASE 3: STRUCTURE ==================== + + private void parseStructure() { + // Parse methods + parseMethodDeclarations(); + + // Parse global fields (outside methods) + parseGlobalFields(); + } + + private void parseMethodDeclarations() { + Pattern methodWithBody = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); + + Matcher m = methodWithBody.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String returnType = m.group(1); + String methodName = m.group(2); + String paramList = m.group(3); + + int bodyStart = text.indexOf('{', m.end() - 1); + int bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) + bodyEnd = text.length(); + + // Parse parameters + List params = parseParameters(paramList, m.start()); + + MethodInfo methodInfo = MethodInfo.declaration( + methodName, + resolveType(returnType), + params, + m.start(), + bodyStart, + bodyEnd + ); + methods.add(methodInfo); + } + + // Also parse JS-style functions + Matcher funcM = FUNC_PARAMS_PATTERN.matcher(text); + while (funcM.find()) { + if (isExcluded(funcM.start())) + continue; + // JS functions don't have explicit return types + String paramList = funcM.group(1); + List params = new ArrayList<>(); + if (paramList != null && !paramList.trim().isEmpty()) { + for (String p : paramList.split(",")) { + String pn = p.trim(); + if (!pn.isEmpty()) { + params.add(FieldInfo.parameter(pn, null, funcM.start(), null)); + } + } + } + // We don't need to store JS functions in methods list - just extract params + } + } + + private List parseParameters(String paramList, int declOffset) { + List params = new ArrayList<>(); + if (paramList == null || paramList.trim().isEmpty()) { + return params; + } + + Pattern paramPattern = Pattern.compile( + "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher m = paramPattern.matcher(paramList); + while (m.find()) { + String typeName = m.group(1); + String paramName = m.group(2); + TypeInfo typeInfo = resolveType(typeName); + params.add(FieldInfo.parameter(paramName, typeInfo, declOffset, null)); + } + return params; + } + + private void parseGlobalFields() { + Matcher m = FIELD_DECL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String typeName = m.group(1); + String fieldName = m.group(2); + int position = m.start(2); + + // Check if inside a method - if so, it's a local, not global + boolean insideMethod = false; + for (MethodInfo method : methods) { + if (method.containsPosition(position)) { + insideMethod = true; + break; + } + } + + if (!insideMethod) { + TypeInfo typeInfo = resolveType(typeName); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position); + globalFields.put(fieldName, fieldInfo); + } + } + } + + private TypeInfo resolveType(String typeName) { + if (typeName == null || typeName.isEmpty()) + return null; + + // Strip generics for resolution + int genericStart = typeName.indexOf('<'); + String baseName = genericStart > 0 ? typeName.substring(0, genericStart) : typeName; + + // Strip array brackets + baseName = baseName.replace("[]", "").trim(); + + // Try to resolve + return typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + } + + // ==================== PHASE 4: BUILD MARKS ==================== + + private List buildMarks() { + List marks = new ArrayList<>(); + + // Comments and strings first (highest priority) + addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT); + addPatternMarks(marks, STRING_PATTERN, TokenType.STRING); + + // Import statements + markImports(marks); + + // Class/interface/enum declarations + markClassDeclarations(marks); + + // Keywords and modifiers + addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); + addPatternMarks(marks, MODIFIER_PATTERN, TokenType.MODIFIER); + + // Type declarations and usages + markTypeDeclarations(marks); + addPatternMarks(marks, NEW_TYPE_PATTERN, TokenType.NEW_TYPE, 1); + + // Methods + markMethodDeclarations(marks); + markMethodCalls(marks); + + // Numbers + addPatternMarks(marks, NUMBER_PATTERN, TokenType.NUMBER); + + // Variables and fields + markVariables(marks); + + // Imported class usages + markImportedClassUsages(marks); + + return marks; + } + + private void addPatternMarks(List marks, Pattern pattern, TokenType type) { + addPatternMarks(marks, pattern, type, 0); + } + + private void addPatternMarks(List marks, Pattern pattern, TokenType type, int group) { + Matcher m = pattern.matcher(text); + while (m.find()) { + marks.add(new ScriptLine.Mark(m.start(group), m.end(group), type)); + } + } + + private void markImports(List marks) { + for (ImportData imp : imports) { + // Mark 'import' keyword + marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.IMPORT_KEYWORD)); + + // Mark the path + int pathStart = imp.getPathStartOffset(); + int pathEnd = imp.getPathEndOffset(); + + if (imp.isResolved()) { + // Resolved import - mark as type declaration + if (imp.getResolvedType() != null) { + marks.add(new ScriptLine.Mark(pathStart, pathEnd, imp.getResolvedType().getTokenType())); + } else { + marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } + } else { + // Unresolved import + marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); + } + } + } + + private void markClassDeclarations(List marks) { + Matcher m = CLASS_DECL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + // Mark the keyword (class/interface/enum) + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.CLASS_KEYWORD)); + + // Mark the class name + String kind = m.group(1); + TokenType nameType; + if ("interface".equals(kind)) { + nameType = TokenType.INTERFACE_DECL; + } else if ("enum".equals(kind)) { + nameType = TokenType.ENUM_DECL; + } else { + nameType = TokenType.CLASS_DECL; + } + marks.add(new ScriptLine.Mark(m.start(2), m.end(2), nameType)); + } + } + + private void markTypeDeclarations(List marks) { + // Pattern for type followed by variable name + Pattern typeDecl = Pattern.compile( + "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + + "([A-Z][a-zA-Z0-9_]*)\\s*(?:<[^>]+>)?\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + + Matcher m = typeDecl.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String typeName = m.group(1); + TypeInfo info = resolveType(typeName); + TokenType tokenType = (info != null && info.isResolved()) ? info.getTokenType() : TokenType.UNDEFINED_VAR; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), tokenType)); + } + } + + private void markMethodDeclarations(List marks) { + Matcher m = METHOD_DECL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + // Return type + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); + // Method name + marks.add(new ScriptLine.Mark(m.start(2), m.end(2), TokenType.METHOD_DECL)); + } + } + + private void markMethodCalls(List marks) { + Matcher m = METHOD_CALL_PATTERN.matcher(text); + while (m.find()) { + int start = m.start(1); + int end = m.end(1); + + if (isExcluded(start)) + continue; + + // Check if this is preceded by a dot (method call on an object) + boolean hasReceiver = false; + String receiverName = null; + int scanPos = start - 1; + while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos))) + scanPos--; + + if (scanPos >= 0 && text.charAt(scanPos) == '.') { + hasReceiver = true; + // Find receiver identifier + int recvEnd = scanPos - 1; + while (recvEnd >= 0 && Character.isWhitespace(text.charAt(recvEnd))) + recvEnd--; + int recvStart = recvEnd; + while (recvStart >= 0 && Character.isJavaIdentifierPart(text.charAt(recvStart))) + recvStart--; + recvStart++; + if (recvStart <= recvEnd && recvStart >= 0) { + receiverName = text.substring(recvStart, recvEnd + 1); + } + } + + // Determine if we should highlight this as a method call + boolean shouldHighlight = true; + + if (hasReceiver && receiverName != null && !receiverName.isEmpty()) { + // Check if receiver is an unresolved class reference + if (Character.isUpperCase(receiverName.charAt(0))) { + TypeInfo recvType = resolveType(receiverName); + if (recvType == null || !recvType.isResolved()) { + // Don't highlight method calls on unresolved types + shouldHighlight = false; + } + } + } + + if (shouldHighlight) { + marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); + } + } + } + + private void markVariables(List marks) { + Set knownKeywords = new HashSet<>(Arrays.asList( + "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", + "null", "true", "false", "if", "else", "switch", "case", "for", "while", "do", + "try", "catch", "finally", "return", "throw", "var", "let", "const", "function", + "continue", "break", "this", "new", "typeof", "instanceof", "class", "interface", + "extends", "implements", "import", "package", "public", "private", "protected", + "static", "final", "abstract", "synchronized", "native", "default", "enum", + "throws", "super", "assert", "volatile", "transient" + )); + + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); + Matcher m = identifier.matcher(text); + + while (m.find()) { + String name = m.group(1); + int position = m.start(1); + + if (isExcluded(position)) + continue; + if (knownKeywords.contains(name)) + continue; + + // Skip if preceded by dot (field access) + if (isPrecededByDot(position)) + continue; + + // Skip import/package statements + if (isInImportOrPackage(position)) + continue; + + // Skip uppercase (type references handled elsewhere) + if (Character.isUpperCase(name.charAt(0))) + continue; + + // Find containing method + MethodInfo containingMethod = findMethodAtPosition(position); + + if (containingMethod != null) { + // Check parameters + if (containingMethod.hasParameter(name)) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER)); + continue; + } + } + + // Check global fields + if (globalFields.containsKey(name)) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + continue; + } + + // Check if it's a method call (followed by paren) + if (isFollowedByParen(m.end(1))) + continue; + + // Unknown identifier - could mark as undefined, but be conservative + // Only mark as undefined if it looks like a variable usage + } + } + + private void markImportedClassUsages(List marks) { + // Find uppercase identifiers followed by dot (static method calls, field access) + Pattern classUsage = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\s*\\."); + Matcher m = classUsage.matcher(text); + + while (m.find()) { + String className = m.group(1); + int start = m.start(1); + int end = m.end(1); + + if (isExcluded(start)) + continue; + + // Try to resolve the class + TypeInfo info = resolveType(className); + if (info != null && info.isResolved()) { + marks.add(new ScriptLine.Mark(start, end, info.getTokenType())); + } else if (!importsBySimpleName.containsKey(className) && + !TypeResolver.JAVA_LANG_CLASSES.contains(className) && + wildcardPackages.isEmpty()) { + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + } + } + } + + // ==================== PHASE 5: CONFLICT RESOLUTION ==================== + + private List resolveConflicts(List marks) { + // Sort by start position, then by descending priority + marks.sort((a, b) -> { + if (a.start != b.start) + return Integer.compare(a.start, b.start); + return Integer.compare(b.type.getPriority(), a.type.getPriority()); + }); + + List result = new ArrayList<>(); + for (ScriptLine.Mark m : marks) { + boolean skip = false; + + // Remove lower-priority overlapping marks + for (int i = result.size() - 1; i >= 0; i--) { + ScriptLine.Mark r = result.get(i); + if (m.start < r.end && m.end > r.start) { + if (r.type.getPriority() < m.type.getPriority()) { + result.remove(i); + } else { + skip = true; + break; + } + } + } + + if (!skip) { + result.add(m); + } + } + + result.sort(Comparator.comparingInt(m -> m.start)); + return result; + } + + // ==================== PHASE 7: INDENT GUIDES ==================== + + private void computeIndentGuides(List marks) { + for (ScriptLine line : lines) { + line.clearIndentGuides(); + } + + // Build ignored ranges from STRING and COMMENT marks + Set ignored = new HashSet<>(); + for (ScriptLine.Mark m : marks) { + if (m.type == TokenType.STRING || m.type == TokenType.COMMENT) { + ignored.add(new int[]{m.start, m.end}); + } + } + + class OpenBrace { + int lineIdx, col; + + OpenBrace(int l, int c) { + lineIdx = l; + col = c; + } + } + Deque stack = new ArrayDeque<>(); + final int tabSize = 4; + + for (int li = 0; li < lines.size(); li++) { + ScriptLine line = lines.get(li); + String s = line.getText(); + + for (int i = 0; i < s.length(); i++) { + int absPos = line.getGlobalStart() + i; + + // Check if in ignored range + boolean isIgnored = false; + for (int[] range : ignored) { + if (absPos >= range[0] && absPos < range[1]) { + isIgnored = true; + break; + } + } + if (isIgnored) + continue; + + char c = s.charAt(i); + if (c == '{') { + int leading = 0; + for (int k = 0; k < i; k++) { + char ch = s.charAt(k); + leading += (ch == '\t') ? tabSize : 1; + } + stack.push(new OpenBrace(li, leading)); + } else if (c == '}') { + if (!stack.isEmpty()) { + OpenBrace open = stack.pop(); + if (open.lineIdx == li) + continue; + + int from = Math.max(0, open.lineIdx + 1); + int to = Math.min(lines.size() - 1, li); + for (int l = from; l <= to; l++) { + lines.get(l).addIndentGuide(open.col); + } + } + } + } + } + } + + // ==================== UTILITY METHODS ==================== + + private boolean isPrecededByDot(int position) { + if (position <= 0) + return false; + int i = position - 1; + while (i >= 0 && Character.isWhitespace(text.charAt(i))) + i--; + return i >= 0 && text.charAt(i) == '.'; + } + + private boolean isFollowedByParen(int position) { + int i = position; + while (i < text.length() && Character.isWhitespace(text.charAt(i))) + i++; + return i < text.length() && text.charAt(i) == '('; + } + + private boolean isInImportOrPackage(int position) { + if (position < 0 || position >= text.length()) + return false; + + int lineStart = text.lastIndexOf('\n', position); + lineStart = lineStart < 0 ? 0 : lineStart + 1; + + int i = lineStart; + while (i < text.length() && Character.isWhitespace(text.charAt(i))) + i++; + + if (text.startsWith("import", i) || text.startsWith("package", i)) { + return true; + } + return false; + } + + private MethodInfo findMethodAtPosition(int position) { + for (MethodInfo method : methods) { + if (method.containsPosition(position)) { + return method; + } + } + return null; + } + + private int findMatchingBrace(int openBraceIndex) { + if (openBraceIndex < 0 || openBraceIndex >= text.length()) + return -1; + + int depth = 0; + for (int i = openBraceIndex; i < text.length(); i++) { + if (isExcluded(i)) + continue; + + char c = text.charAt(i); + if (c == '{') + depth++; + else if (c == '}') { + depth--; + if (depth == 0) + return i; + } + } + return -1; + } + + // ==================== ACCESSORS ==================== + + public List getLines() { + return Collections.unmodifiableList(lines); + } + + public List getImports() { + return Collections.unmodifiableList(imports); + } + + public List getMethods() { + return Collections.unmodifiableList(methods); + } + + public Map getGlobalFields() { + return Collections.unmodifiableMap(globalFields); + } + + public TypeResolver getTypeResolver() { + return typeResolver; + } + + public ScriptLine getLine(int index) { + for (ScriptLine line : lines) { + if (line.getLineIndex() == index) { + return line; + } + } + return null; + } + + public ScriptLine getLineAt(int globalPosition) { + for (ScriptLine line : lines) { + if (line.containsPosition(globalPosition)) { + return line; + } + } + return null; + } + + public int getLineIndexAt(int globalPosition) { + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).containsPosition(globalPosition)) { + return i; + } + } + return -1; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java new file mode 100644 index 000000000..019c4d7aa --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -0,0 +1,303 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.ClientProxy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents a single line of source code with its tokens. + * Handles token management, navigation, and rendering. + * + * A ScriptLine is responsible for: + * - Storing tokens that fall within this line's character range + * - Managing token navigation (prev/next within line) + * - Rendering the line with proper syntax highlighting + * - Computing indent guides for this line + */ +public class ScriptLine { + + private static final char COLOR_CHAR = '\u00A7'; + + private final String text; // The actual text of this line + private final int globalStart; // Start offset in the full document + private final int globalEnd; // End offset in the full document (exclusive) + private final int lineIndex; // 0-based line number + + private final List tokens = new ArrayList<>(); + private final List indentGuides = new ArrayList<>(); // Column positions for indent guides + + // Navigation + private ScriptLine prev; + private ScriptLine next; + private ScriptDocument parent; + + public ScriptLine(String text, int globalStart, int globalEnd, int lineIndex) { + this.text = text; + this.globalStart = globalStart; + this.globalEnd = globalEnd; + this.lineIndex = lineIndex; + } + + // ==================== GETTERS ==================== + + public String getText() { return text; } + public int getGlobalStart() { return globalStart; } + public int getGlobalEnd() { return globalEnd; } + public int getLineIndex() { return lineIndex; } + public int getLength() { return text.length(); } + public List getTokens() { return Collections.unmodifiableList(tokens); } + public List getIndentGuides() { return Collections.unmodifiableList(indentGuides); } + public ScriptDocument getParent() { return parent; } + + void setParent(ScriptDocument parent) { this.parent = parent; } + void setPrev(ScriptLine prev) { this.prev = prev; } + void setNext(ScriptLine next) { this.next = next; } + + // ==================== NAVIGATION ==================== + + public ScriptLine prev() { return prev; } + public ScriptLine next() { return next; } + + public Token getFirstToken() { + return tokens.isEmpty() ? null : tokens.get(0); + } + + public Token getLastToken() { + return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1); + } + + public Token getTokenAt(int globalPosition) { + for (Token t : tokens) { + if (globalPosition >= t.getGlobalStart() && globalPosition < t.getGlobalEnd()) { + return t; + } + } + return null; + } + + // ==================== TOKEN MANAGEMENT ==================== + + /** + * Clear all tokens from this line. + */ + public void clearTokens() { + tokens.clear(); + } + + /** + * Add a token to this line and set up navigation links. + */ + public void addToken(Token token) { + if (!tokens.isEmpty()) { + Token last = tokens.get(tokens.size() - 1); + last.setNext(token); + token.setPrev(last); + } + token.setParentLine(this); + tokens.add(token); + } + + /** + * Build tokens from a list of marks (highlight regions). + * Fills gaps between marks with DEFAULT tokens. + * + * @param marks List of highlight marks that overlap this line + * @param fullText The complete document text + */ + public void buildTokensFromMarks(List marks, String fullText) { + clearTokens(); + int cursor = globalStart; + + for (Mark mark : marks) { + // Skip marks that don't overlap this line + if (mark.end <= globalStart || mark.start >= globalEnd) { + continue; + } + + // Clamp mark to line boundaries + int tokenStart = Math.max(mark.start, globalStart); + int tokenEnd = Math.min(mark.end, globalEnd); + + // Validate bounds + tokenStart = Math.max(0, Math.min(tokenStart, fullText.length())); + tokenEnd = Math.max(0, Math.min(tokenEnd, fullText.length())); + + // Add default token for any gap before this mark + if (cursor < tokenStart) { + int gapEnd = Math.min(tokenStart, fullText.length()); + String gapText = fullText.substring(cursor, gapEnd); + addToken(Token.defaultToken(gapText, cursor, gapEnd)); + } + + // Add the marked token + if (tokenStart < tokenEnd) { + String tokenText = fullText.substring(tokenStart, tokenEnd); + Token token = new Token(tokenText, tokenStart, tokenEnd, mark.type); + addToken(token); + } + + cursor = tokenEnd; + } + + // Add trailing default token if needed + if (cursor < globalEnd) { + int end = Math.min(globalEnd, fullText.length()); + if (cursor < end) { + String trailingText = fullText.substring(cursor, end); + addToken(Token.defaultToken(trailingText, cursor, end)); + } + } + } + + // ==================== INDENT GUIDES ==================== + + /** + * Clear indent guides for this line. + */ + public void clearIndentGuides() { + indentGuides.clear(); + } + + /** + * Add an indent guide at the specified column. + */ + public void addIndentGuide(int column) { + if (!indentGuides.contains(column)) { + indentGuides.add(column); + } + } + + // ==================== RENDERING ==================== + + /** + * Draw this line with syntax highlighting using Minecraft color codes. + * Compatible with the existing rendering system. + */ + public void drawString(int x, int y, int defaultColor) { + StringBuilder builder = new StringBuilder(); + int lastIndex = 0; + + for (Token t : tokens) { + int tokenStart = t.getGlobalStart() - globalStart; // relative position in line + + // Append any text before this token (spaces, punctuation, etc.) + if (tokenStart > lastIndex && tokenStart <= text.length()) { + builder.append(text, lastIndex, tokenStart); + } + + // Append the colored token + builder.append(COLOR_CHAR) + .append(t.getColorCode()) + .append(t.getText()) + .append(COLOR_CHAR) + .append('f'); // Reset to white + + lastIndex = tokenStart + t.getText().length(); + } + + // Append any remaining text after the last token + if (lastIndex < text.length()) { + builder.append(text.substring(lastIndex)); + } + + ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); + } + + /** + * Draw this line with hex colors instead of Minecraft color codes. + * More flexible but requires custom font rendering. + * + * @param x X position + * @param y Y position + * @param renderer A functional interface for drawing colored text segments + */ + public void drawStringHex(int x, int y, HexColorRenderer renderer) { + int currentX = x; + int lastIndex = 0; + + for (Token t : tokens) { + int tokenStart = t.getGlobalStart() - globalStart; + + // Draw any text before this token in default color + if (tokenStart > lastIndex && tokenStart <= text.length()) { + String gap = text.substring(lastIndex, tokenStart); + currentX = renderer.draw(gap, currentX, y, 0xFFFFFF); + } + + // Draw the colored token + currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor()); + + lastIndex = tokenStart + t.getText().length(); + } + + // Draw any remaining text in default color + if (lastIndex < text.length()) { + renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); + } + } + + /** + * Functional interface for rendering text with hex colors. + */ + @FunctionalInterface + public interface HexColorRenderer { + /** + * Draw text at the specified position with the given color. + * @return The X position after drawing (for continuation) + */ + int draw(String text, int x, int y, int hexColor); + } + + // ==================== UTILITIES ==================== + + /** + * Check if a global position falls within this line. + */ + public boolean containsPosition(int globalPosition) { + return globalPosition >= globalStart && globalPosition < globalEnd; + } + + /** + * Convert a global position to a column (local offset) within this line. + */ + public int toColumn(int globalPosition) { + return Math.max(0, Math.min(globalPosition - globalStart, text.length())); + } + + /** + * Convert a column (local offset) to a global position. + */ + public int toGlobal(int column) { + return globalStart + Math.max(0, Math.min(column, text.length())); + } + + @Override + public String toString() { + return "ScriptLine{" + lineIndex + ": '" + text + "' [" + globalStart + "-" + globalEnd + "], " + tokens.size() + " tokens}"; + } + + // ==================== MARK CLASS (for token building) ==================== + + /** + * A simple mark representing a highlighted region. + * Used during token building phase. + */ + public static class Mark { + public final int start; + public final int end; + public final TokenType type; + + public Mark(int start, int end, TokenType type) { + this.start = start; + this.end = end; + this.type = type; + } + + @Override + public String toString() { + return "Mark{" + type + " [" + start + "-" + end + "]}"; + } + } +} 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 new file mode 100644 index 000000000..e3808a35f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTextContainer.java @@ -0,0 +1,199 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.JavaTextContainer; + +import java.util.ArrayList; +import java.util.List; + +/** + * New interpreter-based text container that replaces JavaTextContainer. + * + * This class provides the same interface as JavaTextContainer but uses the + * new modular interpreter system internally. It produces compatible LineData + * objects for seamless integration with GuiScriptTextArea. + * + * Key differences from JavaTextContainer: + * - Clean single-pass tokenization with 7 distinct phases + * - Proper token navigation via prev()/next() methods + * - Type-specific metadata on tokens (TypeInfo, FieldInfo, MethodInfo) + * - Better separation of concerns (TypeResolver, ScriptDocument, ScriptLine) + * - Hex color support in addition to Minecraft color codes + */ +public class ScriptTextContainer extends JavaTextContainer { + + /** + * Feature flag to enable/disable the new interpreter system. + * When false, falls back to original JavaTextContainer behavior. + */ + public static boolean USE_NEW_INTERPRETER = true; + + private ScriptDocument document; + + public ScriptTextContainer(String text) { + super(text); + if (USE_NEW_INTERPRETER) { + document = new ScriptDocument(text); + } + } + + @Override + public void init(int width, int height) { + if (!USE_NEW_INTERPRETER) { + super.init(width, height); + return; + } + + lineHeight = ClientProxy.Font.height(); + if (lineHeight == 0) lineHeight = 12; + + // Initialize the document + document.setText(text); + document.init(width, height); + + // Convert ScriptLines to LineData for compatibility + rebuildLineData(); + + linesCount = lines.size(); + totalHeight = linesCount * lineHeight; + visibleLines = Math.max(height / lineHeight - 1, 1); + } + + /** + * Initialize with explicit text (called when text changes). + */ + @Override + public void init(String text, int width, int height) { + this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); + + if (!USE_NEW_INTERPRETER) { + super.init(text, width, height); + return; + } + + document = new ScriptDocument(this.text); + init(width, height); + } + + /** + * Convert ScriptDocument lines to the legacy LineData format. + */ + private void rebuildLineData() { + lines.clear(); + for (ScriptLine scriptLine : document.getLines()) { + LineData ld = new LineData( + scriptLine.getText(), + scriptLine.getGlobalStart(), + scriptLine.getGlobalEnd() + ); + + // Copy indent guides + ld.indentCols.addAll(scriptLine.getIndentGuides()); + + // Convert tokens - use fully qualified name to avoid conflict with inherited Token + for (noppes.npcs.client.gui.util.script.interpreter.Token interpreterToken : scriptLine.getTokens()) { + ld.tokens.add(new JavaTextContainer.Token( + interpreterToken.getText(), + toLegacyTokenType(interpreterToken.getType()), + interpreterToken.getGlobalStart(), + interpreterToken.getGlobalEnd() + )); + } + + lines.add(ld); + } + } + + /** + * Main formatting entry point - matches JavaTextContainer signature. + */ + @Override + public void formatCodeText() { + if (!USE_NEW_INTERPRETER) { + super.formatCodeText(); + return; + } + + document.formatCodeText(); + rebuildLineData(); + } + + /** + * Convert new interpreter TokenType to legacy JavaTextContainer.TokenType format. + */ + private JavaTextContainer.TokenType toLegacyTokenType(TokenType type) { + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.COMMENT) return JavaTextContainer.TokenType.COMMENT; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.STRING) return JavaTextContainer.TokenType.STRING; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_KEYWORD) return JavaTextContainer.TokenType.CLASS_KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.IMPORT_KEYWORD) return JavaTextContainer.TokenType.IMPORT_KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.KEYWORD) return JavaTextContainer.TokenType.KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.MODIFIER) return JavaTextContainer.TokenType.MODIFIER; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.NEW_TYPE) return JavaTextContainer.TokenType.NEW_TYPE; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.INTERFACE_DECL) return JavaTextContainer.TokenType.INTERFACE_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.ENUM_DECL) return JavaTextContainer.TokenType.ENUM_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_DECL) return JavaTextContainer.TokenType.CLASS_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.IMPORTED_CLASS) return JavaTextContainer.TokenType.IMPORTED_CLASS; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.TYPE_DECL) return JavaTextContainer.TokenType.TYPE_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_DECL) return JavaTextContainer.TokenType.METHOD_DECARE; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_CALL) return JavaTextContainer.TokenType.METHOD_CALL; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.NUMBER) return JavaTextContainer.TokenType.NUMBER; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.GLOBAL_FIELD) return JavaTextContainer.TokenType.GLOBAL_FIELD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.LOCAL_FIELD) return JavaTextContainer.TokenType.LOCAL_FIELD; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.PARAMETER) return JavaTextContainer.TokenType.PARAMETER; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.UNDEFINED_VAR) return JavaTextContainer.TokenType.UNDEFINED_VAR; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.VARIABLE) return JavaTextContainer.TokenType.VARIABLE; + return JavaTextContainer.TokenType.DEFAULT; + } + + // ==================== ACCESSORS ==================== + + /** + * Get the underlying ScriptDocument for advanced operations. + */ + public ScriptDocument getDocument() { + return document; + } + + /** + * Get method blocks for compatibility with existing code. + * Creates MethodBlock-like objects from the new MethodInfo. + */ + public List getMethodBlocks() { + if (document == null) { + return new ArrayList<>(); + } + List blocks = new ArrayList<>(); + for (MethodInfo method : document.getMethods()) { + blocks.add(new MethodBlockCompat(method)); + } + return blocks; + } + + /** + * Compatibility wrapper for MethodBlock. + */ + public static class MethodBlockCompat { + private final MethodInfo methodInfo; + public List parameters = new ArrayList<>(); + public List localVariables = new ArrayList<>(); + + public MethodBlockCompat(MethodInfo methodInfo) { + this.methodInfo = methodInfo; + for (FieldInfo param : methodInfo.getParameters()) { + parameters.add(param.getName()); + } + } + + public int getStartOffset() { return methodInfo.getDeclarationOffset(); } + public int getEndOffset() { return methodInfo.getBodyEnd(); } + + public boolean containsPosition(int position) { + return methodInfo.containsPosition(position); + } + + public boolean isLocalDeclaredAtPosition(String varName, int position) { + // Simplified - would need local variable tracking for full support + return localVariables.contains(varName); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java new file mode 100644 index 000000000..cbd8cfc4f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -0,0 +1,237 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Represents a single token in the source code with its type and metadata. + * Tokens are the atomic units of the syntax highlighting system. + * + * Each token knows: + * - Its text content + * - Its position in the global source + * - Its type (for coloring) + * - Type-specific metadata (resolved class info, declaration info, etc.) + */ +public class Token { + + private final String text; + private final int globalStart; // start offset in the full document + private final int globalEnd; // end offset in the full document + private TokenType type; + + // Optional metadata based on token type + private TypeInfo typeInfo; // For class references, type declarations + private FieldInfo fieldInfo; // For field references + private MethodInfo methodInfo; // For method calls/declarations + private ImportData importData; // For import statements + + // Navigation - set by ScriptLine + private Token prev; + private Token next; + private ScriptLine parentLine; + + public Token(String text, int globalStart, int globalEnd, TokenType type) { + this.text = text; + this.globalStart = globalStart; + this.globalEnd = globalEnd; + this.type = type; + } + + // ==================== FACTORY METHODS ==================== + + public static Token defaultToken(String text, int start, int end) { + return new Token(text, start, end, TokenType.DEFAULT); + } + + public static Token keyword(String text, int start, int end) { + return new Token(text, start, end, TokenType.KEYWORD); + } + + public static Token modifier(String text, int start, int end) { + return new Token(text, start, end, TokenType.MODIFIER); + } + + public static Token comment(String text, int start, int end) { + return new Token(text, start, end, TokenType.COMMENT); + } + + public static Token string(String text, int start, int end) { + return new Token(text, start, end, TokenType.STRING); + } + + public static Token number(String text, int start, int end) { + return new Token(text, start, end, TokenType.NUMBER); + } + + public static Token typeReference(String text, int start, int end, TypeInfo info) { + Token t = new Token(text, start, end, info != null ? info.getTokenType() : TokenType.TYPE_DECL); + t.typeInfo = info; + return t; + } + + public static Token methodDecl(String text, int start, int end, MethodInfo info) { + Token t = new Token(text, start, end, TokenType.METHOD_DECL); + t.methodInfo = info; + return t; + } + + public static Token methodCall(String text, int start, int end, MethodInfo info, boolean resolved) { + Token t = new Token(text, start, end, resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT); + t.methodInfo = info; + return t; + } + + public static Token globalField(String text, int start, int end, FieldInfo info) { + Token t = new Token(text, start, end, TokenType.GLOBAL_FIELD); + t.fieldInfo = info; + return t; + } + + public static Token localField(String text, int start, int end, FieldInfo info) { + Token t = new Token(text, start, end, TokenType.LOCAL_FIELD); + t.fieldInfo = info; + return t; + } + + public static Token parameter(String text, int start, int end, FieldInfo info) { + Token t = new Token(text, start, end, TokenType.PARAMETER); + t.fieldInfo = info; + return t; + } + + public static Token undefined(String text, int start, int end) { + return new Token(text, start, end, TokenType.UNDEFINED_VAR); + } + + // ==================== GETTERS ==================== + + public String getText() { return text; } + public int getGlobalStart() { return globalStart; } + public int getGlobalEnd() { return globalEnd; } + public int getLength() { return globalEnd - globalStart; } + public TokenType getType() { return type; } + public TypeInfo getTypeInfo() { return typeInfo; } + public FieldInfo getFieldInfo() { return fieldInfo; } + public MethodInfo getMethodInfo() { return methodInfo; } + public ImportData getImportData() { return importData; } + public ScriptLine getParentLine() { return parentLine; } + + // ==================== SETTERS ==================== + + public void setType(TokenType type) { this.type = type; } + public void setTypeInfo(TypeInfo info) { this.typeInfo = info; } + public void setFieldInfo(FieldInfo info) { this.fieldInfo = info; } + public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } + public void setImportData(ImportData data) { this.importData = data; } + + void setParentLine(ScriptLine line) { this.parentLine = line; } + void setPrev(Token prev) { this.prev = prev; } + void setNext(Token next) { this.next = next; } + + // ==================== NAVIGATION ==================== + + /** + * Get the previous token (may be on a previous line). + */ + public Token prev() { + if (prev != null) return prev; + if (parentLine == null) return null; + + ScriptLine prevLine = parentLine.prev(); + while (prevLine != null) { + Token last = prevLine.getLastToken(); + if (last != null) return last; + prevLine = prevLine.prev(); + } + return null; + } + + /** + * Get the next token (may be on a following line). + */ + public Token next() { + if (next != null) return next; + if (parentLine == null) return null; + + ScriptLine nextLine = parentLine.next(); + while (nextLine != null) { + Token first = nextLine.getFirstToken(); + if (first != null) return first; + nextLine = nextLine.next(); + } + return null; + } + + /** + * Get the previous token on the same line only. + */ + public Token prevOnLine() { + return prev; + } + + /** + * Get the next token on the same line only. + */ + public Token nextOnLine() { + return next; + } + + // ==================== TYPE CHECKS ==================== + + public boolean isKeyword() { + return type == TokenType.KEYWORD || type == TokenType.MODIFIER || + type == TokenType.CLASS_KEYWORD || type == TokenType.IMPORT_KEYWORD; + } + + public boolean isTypeReference() { + return type == TokenType.TYPE_DECL || type == TokenType.IMPORTED_CLASS || + type == TokenType.CLASS_DECL || type == TokenType.INTERFACE_DECL || + type == TokenType.ENUM_DECL || type == TokenType.NEW_TYPE; + } + + public boolean isResolved() { + if (typeInfo != null) return typeInfo.isResolved(); + if (fieldInfo != null) return fieldInfo.isResolved(); + if (methodInfo != null) return methodInfo.isResolved(); + return type != TokenType.UNDEFINED_VAR; + } + + public boolean isIdentifier() { + if (text.isEmpty()) return false; + char first = text.charAt(0); + return Character.isJavaIdentifierStart(first); + } + + public boolean startsWithUpperCase() { + return !text.isEmpty() && Character.isUpperCase(text.charAt(0)); + } + + public boolean isMethodCall() { + return type == TokenType.METHOD_CALL || + (type == TokenType.DEFAULT && methodInfo != null); + } + + public boolean isField() { + return type == TokenType.GLOBAL_FIELD || type == TokenType.LOCAL_FIELD || + type == TokenType.PARAMETER; + } + + // ==================== RENDERING ==================== + + /** + * Get the hex color for this token. + */ + public int getHexColor() { + return type.getHexColor(); + } + + /** + * Get the Minecraft color code character. + */ + public char getColorCode() { + return type.toColorCode(); + } + + @Override + public String toString() { + return "Token{'" + text + "', " + type + ", [" + globalStart + "-" + globalEnd + "]}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java new file mode 100644 index 000000000..2f91597d2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -0,0 +1,103 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Defines all token types for syntax highlighting with hex colors and priorities. + * Priority determines which token type wins when marks overlap. + * Higher priority = wins conflicts. + */ +public enum TokenType { + // Comments and strings have highest priority - they override everything inside them + COMMENT(0x777777, 130), + STRING(0xCC8855, 120), + + // Keywords and modifiers + CLASS_KEYWORD(0xFF5555, 115), // 'class', 'interface', 'enum' keywords + IMPORT_KEYWORD(0xFFAA00, 110), // 'import' keyword + KEYWORD(0xFF5555, 100), // control flow: if, else, for, while, etc. + MODIFIER(0xFFAA00, 90), // public, private, static, final, etc. + + // Type declarations and references + NEW_TYPE(0xFF55FF, 80), // type after 'new' keyword + INTERFACE_DECL(0x55FFFF, 85), // interface names (aqua) + ENUM_DECL(0xFF55FF, 85), // enum names (magenta) + CLASS_DECL(0x00AAAA, 85), // class names in declarations + IMPORTED_CLASS(0x00AAAA, 75), // imported class usages + TYPE_DECL(0x00AAAA, 70), // package paths, type references + + // Methods + METHOD_DECL(0x00AA00, 60), // method declarations (green) + METHOD_CALL(0x55FF55, 50), // method calls (bright green) + + // Variables and fields + UNDEFINED_VAR(0xAA0000, 105), // unresolved variables (dark red) - high priority + PARAMETER(0x5555FF, 36), // method parameters (blue) + GLOBAL_FIELD(0x55FFFF, 35), // class-level fields (aqua) + LOCAL_FIELD(0xFFFF55, 25), // local variables (yellow) + + // Literals + NUMBER(0x777777, 40), // numeric literals + + // Default + VARIABLE(0xFFFFFF, 30), // generic variables + DEFAULT(0xFFFFFF, 0); // default text color (white) + + private final int hexColor; + private final int priority; + + TokenType(int hexColor, int priority) { + this.hexColor = hexColor; + this.priority = priority; + } + + public int getHexColor() { + return hexColor; + } + + public int getPriority() { + return priority; + } + + /** + * Convert this token type to a Minecraft color code character. + * Used for backward compatibility with the existing rendering system. + */ + public char toColorCode() { + switch (this) { + case COMMENT: + case NUMBER: + return '7'; // gray + case STRING: + return '5'; // purple + case CLASS_KEYWORD: + case KEYWORD: + return 'c'; // red + case IMPORT_KEYWORD: + case MODIFIER: + return '6'; // gold + case NEW_TYPE: + case ENUM_DECL: + return 'd'; // magenta + case INTERFACE_DECL: + case GLOBAL_FIELD: + return 'b'; // aqua + case CLASS_DECL: + case IMPORTED_CLASS: + case TYPE_DECL: + return '3'; // dark aqua + case METHOD_DECL: + return '2'; // dark green + case METHOD_CALL: + return 'a'; // green + case LOCAL_FIELD: + return 'e'; // yellow + case PARAMETER: + return '9'; // blue + case UNDEFINED_VAR: + return '4'; // dark red + case VARIABLE: + case DEFAULT: + default: + return 'f'; // white + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java new file mode 100644 index 000000000..544bdadf3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -0,0 +1,173 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Represents resolved type information for a class/interface/enum. + * Immutable data class holding all type metadata. + */ +public final class TypeInfo { + + public enum Kind { + CLASS, + INTERFACE, + ENUM, + UNKNOWN + } + + private final String simpleName; // e.g., "List", "ColorType" + private final String fullName; // e.g., "java.util.List", "kamkeel...IOverlay$ColorType" + private final String packageName; // e.g., "java.util", "kamkeel.npcdbc.api.client.overlay" + private final Kind kind; // CLASS, INTERFACE, ENUM + private final Class javaClass; // The actual resolved Java class (null if unresolved) + private final boolean resolved; // Whether this type was successfully resolved + private final TypeInfo enclosingType; // For inner classes, the outer type (null if top-level) + + private TypeInfo(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { + this.simpleName = simpleName; + this.fullName = fullName; + this.packageName = packageName; + this.kind = kind; + this.javaClass = javaClass; + this.resolved = resolved; + this.enclosingType = enclosingType; + } + + // Factory methods + public static TypeInfo resolved(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass) { + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, null); + } + + public static TypeInfo resolvedInner(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, TypeInfo enclosing) { + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, enclosing); + } + + public static TypeInfo unresolved(String simpleName, String fullPath) { + int lastDot = fullPath.lastIndexOf('.'); + String pkg = lastDot > 0 ? fullPath.substring(0, lastDot) : ""; + return new TypeInfo(simpleName, fullPath, pkg, Kind.UNKNOWN, null, false, null); + } + + public static TypeInfo fromClass(Class clazz) { + if (clazz == null) return null; + + Kind kind; + if (clazz.isInterface()) { + kind = Kind.INTERFACE; + } else if (clazz.isEnum()) { + kind = Kind.ENUM; + } else { + kind = Kind.CLASS; + } + + String fullName = clazz.getName(); + String simpleName = clazz.getSimpleName(); + Package pkg = clazz.getPackage(); + String packageName = pkg != null ? pkg.getName() : ""; + + TypeInfo enclosing = null; + if (clazz.getEnclosingClass() != null) { + enclosing = fromClass(clazz.getEnclosingClass()); + } + + return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing); + } + + // Getters + public String getSimpleName() { return simpleName; } + public String getFullName() { return fullName; } + public String getPackageName() { return packageName; } + public Kind getKind() { return kind; } + public Class getJavaClass() { return javaClass; } + public boolean isResolved() { return resolved; } + public TypeInfo getEnclosingType() { return enclosingType; } + public boolean isInnerClass() { return enclosingType != null; } + + /** + * Get the appropriate TokenType for highlighting this type. + */ + public TokenType getTokenType() { + if (!resolved) { + return TokenType.UNDEFINED_VAR; + } + switch (kind) { + case INTERFACE: + return TokenType.INTERFACE_DECL; + case ENUM: + return TokenType.ENUM_DECL; + case CLASS: + default: + return TokenType.IMPORTED_CLASS; + } + } + + /** + * Check if this type has a method with the given name. + */ + public boolean hasMethod(String methodName) { + if (javaClass == null) return false; + try { + for (java.lang.reflect.Method m : javaClass.getMethods()) { + if (m.getName().equals(methodName)) { + return true; + } + } + } catch (Exception e) { + // Security or linkage error + } + return false; + } + + /** + * Check if this type has a method with the given name and parameter count. + */ + public boolean hasMethod(String methodName, int paramCount) { + if (javaClass == null) return false; + try { + for (java.lang.reflect.Method m : javaClass.getMethods()) { + if (m.getName().equals(methodName) && m.getParameterCount() == paramCount) { + return true; + } + } + } catch (Exception e) { + // Security or linkage error + } + return false; + } + + /** + * Check if this type has a field with the given name. + */ + public boolean hasField(String fieldName) { + if (javaClass == null) return false; + try { + for (java.lang.reflect.Field f : javaClass.getFields()) { + if (f.getName().equals(fieldName)) { + return true; + } + } + } catch (Exception e) { + // Security or linkage error + } + return false; + } + + @Override + public String toString() { + return "TypeInfo{" + fullName + ", " + kind + ", resolved=" + resolved + "}"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TypeInfo typeInfo = (TypeInfo) o; + return fullName.equals(typeInfo.fullName); + } + + @Override + public int hashCode() { + return fullName.hashCode(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java new file mode 100644 index 000000000..0c8794c85 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java @@ -0,0 +1,389 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.util.*; + +/** + * Resolves class/interface/enum types from import paths and simple names. + * Maintains caches for performance and handles inner class resolution. + * + * This is a clean reimplementation of ClassPathFinder with better OOP structure. + */ +public class TypeResolver { + + // Cache: fully-qualified class name -> TypeInfo + private final Map typeCache = new HashMap<>(); + + // Cache: validated package paths + private final Set validPackages = new HashSet<>(); + + // Auto-imported java.lang classes + public static final Set JAVA_LANG_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", + "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", + "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", + "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", + "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", + "ClassCastException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", + "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" + ))); + + // Singleton instance for global caching (optional - can also use per-document instances) + private static TypeResolver instance; + + public static TypeResolver getInstance() { + if (instance == null) { + instance = new TypeResolver(); + } + return instance; + } + + public TypeResolver() { + // Pre-register common packages + validPackages.add("java"); + validPackages.add("java.lang"); + validPackages.add("java.util"); + validPackages.add("java.io"); + } + + // ==================== CACHE MANAGEMENT ==================== + + /** + * Clear all caches. Call when imports change significantly. + */ + public void clearCache() { + typeCache.clear(); + validPackages.clear(); + // Re-add common packages + validPackages.add("java"); + validPackages.add("java.lang"); + validPackages.add("java.util"); + validPackages.add("java.io"); + } + + // ==================== TYPE RESOLUTION ==================== + + /** + * Resolve a fully-qualified class name. + * Handles inner classes with both dot and dollar notation. + */ + public TypeInfo resolveFullName(String fullName) { + if (fullName == null || fullName.isEmpty()) { + return null; + } + + // Normalize: remove whitespace around dots + String normalized = fullName.replaceAll("\\s*\\.\\s*", ".").trim(); + + // Check cache first + if (typeCache.containsKey(normalized)) { + return typeCache.get(normalized); + } + + // Try direct resolution + TypeInfo result = tryResolveClass(normalized); + if (result != null) { + return result; + } + + // Try converting dots to $ for inner classes + // Work backwards from the end, trying each segment as an inner class + String[] segments = normalized.split("\\."); + for (int i = segments.length - 1; i > 0; i--) { + StringBuilder candidate = new StringBuilder(); + // Package portion + for (int j = 0; j < i; j++) { + if (j > 0) candidate.append('.'); + candidate.append(segments[j]); + } + // Class portion with $ + for (int j = i; j < segments.length; j++) { + candidate.append(j == i ? '.' : '$'); + candidate.append(segments[j]); + } + + result = tryResolveClass(candidate.toString()); + if (result != null) { + // Cache the original lookup + typeCache.put(normalized, result); + return result; + } + } + + // Not found - cache the miss + typeCache.put(normalized, null); + return null; + } + + /** + * Resolve a simple class name using provided import context. + */ + public TypeInfo resolveSimpleName(String simpleName, + Map imports, + Set wildcardPackages) { + if (simpleName == null || simpleName.isEmpty()) { + return null; + } + + // 1. Check explicit imports + ImportData importData = imports.get(simpleName); + if (importData != null && importData.getResolvedType() != null) { + return importData.getResolvedType(); + } + if (importData != null) { + TypeInfo resolved = resolveFullName(importData.getFullPath()); + if (resolved != null) { + importData.setResolvedType(resolved); + return resolved; + } + } + + // 2. Check java.lang + if (JAVA_LANG_CLASSES.contains(simpleName)) { + TypeInfo langType = resolveFullName("java.lang." + simpleName); + if (langType != null) { + return langType; + } + } + + // 3. Check wildcard packages + if (wildcardPackages != null) { + for (String pkg : wildcardPackages) { + // Try as package.ClassName + TypeInfo fromPkg = resolveFullName(pkg + "." + simpleName); + if (fromPkg != null) { + return fromPkg; + } + + // Try as OuterClass$InnerClass (for class-level wildcards like IOverlay.*) + TypeInfo fromInner = resolveFullName(pkg + "$" + simpleName); + if (fromInner != null) { + return fromInner; + } + } + } + + return null; + } + + /** + * Try to load a class and create TypeInfo for it. + */ + private TypeInfo tryResolveClass(String className) { + if (className == null || className.isEmpty()) { + return null; + } + + // Check cache + if (typeCache.containsKey(className)) { + return typeCache.get(className); + } + + try { + Class clazz = Class.forName(className); + TypeInfo info = TypeInfo.fromClass(clazz); + typeCache.put(className, info); + + // Register the package as valid + registerPackage(info.getPackageName()); + + return info; + } catch (ClassNotFoundException e) { + // Cache the miss + typeCache.put(className, null); + return null; + } catch (LinkageError e) { + // NoClassDefFoundError is a subclass of LinkageError + typeCache.put(className, null); + return null; + } + } + + // ==================== PACKAGE VALIDATION ==================== + + /** + * Check if a package path is valid. + */ + public boolean isValidPackage(String packagePath) { + if (packagePath == null || packagePath.isEmpty()) { + return false; + } + + if (validPackages.contains(packagePath)) { + return true; + } + + // Try to find a class in this package to validate it + String[] testClasses = getTestClassesForPackage(packagePath); + for (String testClass : testClasses) { + try { + Class.forName(testClass); + registerPackage(packagePath); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + } + } + + return false; + } + + /** + * Register a package path as valid (and all parent packages). + */ + private void registerPackage(String packagePath) { + if (packagePath == null || packagePath.isEmpty()) { + return; + } + validPackages.add(packagePath); + + // Also register parent packages + int lastDot; + String current = packagePath; + while ((lastDot = current.lastIndexOf('.')) > 0) { + current = current.substring(0, lastDot); + validPackages.add(current); + } + } + + private String[] getTestClassesForPackage(String packagePath) { + switch (packagePath) { + case "java": + return new String[]{"java.lang.Object"}; + case "java.util": + return new String[]{"java.util.List", "java.util.Map"}; + case "java.io": + return new String[]{"java.io.File", "java.io.InputStream"}; + case "java.net": + return new String[]{"java.net.URL", "java.net.Socket"}; + case "java.lang": + return new String[]{"java.lang.Object", "java.lang.String"}; + default: + return new String[]{}; + } + } + + // ==================== IMPORT RESOLUTION ==================== + + /** + * Resolve all imports and return a map of simple name -> ImportData. + */ + public Map resolveImports(List imports) { + Map resolved = new HashMap<>(); + + for (ImportData imp : imports) { + if (imp.isWildcard()) { + // Wildcard imports: validate the package exists + imp.markResolved(isValidPackage(imp.getFullPath()) || + resolveFullName(imp.getFullPath()) != null); + } else { + // Specific import: resolve the class + TypeInfo typeInfo = resolveFullName(imp.getFullPath()); + imp.setResolvedType(typeInfo); + if (imp.getSimpleName() != null) { + resolved.put(imp.getSimpleName(), imp); + } + } + } + + return resolved; + } + + // ==================== GENERIC TYPE PARSING ==================== + + /** + * Parse type names from generic content like "Map>". + * Returns TypeInfo for each type found. + */ + public List parseGenericTypes(String content, + Map imports, + Set wildcardPackages) { + List results = new ArrayList<>(); + parseGenericTypesRecursive(content, 0, imports, wildcardPackages, results); + return results; + } + + private void parseGenericTypesRecursive(String content, int baseOffset, + Map imports, + Set wildcardPackages, + List results) { + if (content == null || content.isEmpty()) return; + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // Skip non-identifier characters + if (!Character.isJavaIdentifierStart(c)) { + i++; + continue; + } + + // Found start of identifier + int start = i; + while (i < content.length() && Character.isJavaIdentifierPart(content.charAt(i))) { + i++; + } + String typeName = content.substring(start, i); + + // Only process uppercase-starting identifiers as types + if (Character.isUpperCase(typeName.charAt(0))) { + TypeInfo info = resolveSimpleName(typeName, imports, wildcardPackages); + results.add(new GenericTypeOccurrence( + baseOffset + start, + baseOffset + i, + typeName, + info + )); + } + + // Skip whitespace + while (i < content.length() && Character.isWhitespace(content.charAt(i))) { + i++; + } + + // Check for nested generic + if (i < content.length() && content.charAt(i) == '<') { + int nestedStart = i + 1; + int depth = 1; + i++; + + while (i < content.length() && depth > 0) { + if (content.charAt(i) == '<') depth++; + else if (content.charAt(i) == '>') depth--; + i++; + } + + // Recursively parse nested content + if (nestedStart < i - 1) { + String nestedContent = content.substring(nestedStart, i - 1); + parseGenericTypesRecursive(nestedContent, baseOffset + nestedStart, + imports, wildcardPackages, results); + } + } + } + } + + /** + * Represents a type occurrence within generic content. + */ + public static class GenericTypeOccurrence { + public final int startOffset; + public final int endOffset; + public final String typeName; + public final TypeInfo typeInfo; + + public GenericTypeOccurrence(int startOffset, int endOffset, String typeName, TypeInfo typeInfo) { + this.startOffset = startOffset; + this.endOffset = endOffset; + this.typeName = typeName; + this.typeInfo = typeInfo; + } + + public TokenType getTokenType() { + if (typeInfo == null || !typeInfo.isResolved()) { + return TokenType.UNDEFINED_VAR; + } + return typeInfo.getTokenType(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java new file mode 100644 index 000000000..0fe302521 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/package-info.java @@ -0,0 +1,102 @@ +/** + *

Interpreter Package - Java Syntax Highlighting System

+ * + *

This package provides a clean, modular rewrite of the JavaTextContainer + * syntax highlighting system. It replaces the original implementation with + * well-structured, atomic classes that work together like LEGO pieces.

+ * + *

Architecture Overview

+ * + *
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                    ScriptTextContainer                          │
+ * │  (Compatibility adapter extending JavaTextContainer)            │
+ * │  - USE_NEW_INTERPRETER flag to toggle between systems           │
+ * │  - Produces JavaTextContainer.LineData/Token for GUI compat     │
+ * └───────────────────────┬─────────────────────────────────────────┘
+ *                         │
+ *                         ▼
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                     ScriptDocument                              │
+ * │  (Main document container - replaces JavaTextContainer core)    │
+ * │                                                                 │
+ * │  7-Phase Tokenization Pipeline:                                 │
+ * │  1. findExcludedRanges() - strings/comments                     │
+ * │  2. parseImports() - import statements                          │
+ * │  3. parseStructure() - methods, fields                          │
+ * │  4. buildMarks() - all highlighting marks                       │
+ * │  5. resolveConflicts() - priority-based resolution              │
+ * │  6. buildTokens() - convert marks to tokens per line            │
+ * │  7. computeIndentGuides() - visual indent guides                │
+ * └───────────────────────┬─────────────────────────────────────────┘
+ *                         │
+ *                         ▼
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                      ScriptLine                                 │
+ * │  (Line container with token management and rendering)           │
+ * │  - Token list with navigation                                   │
+ * │  - Indent guides                                                │
+ * │  - drawString() with color codes                                │
+ * │  - drawStringHex() for direct hex color rendering               │
+ * └───────────────────────┬─────────────────────────────────────────┘
+ *                         │
+ *                         ▼
+ * ┌─────────────────────────────────────────────────────────────────┐
+ * │                        Token                                    │
+ * │  (Atomic token unit with type-specific metadata)                │
+ * │  - Text, start/end offsets                                      │
+ * │  - TokenType (with hex color and priority)                      │
+ * │  - prev()/next() navigation                                     │
+ * │  - Type-specific metadata: TypeInfo, FieldInfo, MethodInfo      │
+ * └─────────────────────────────────────────────────────────────────┘
+ * 
+ * + *

Supporting Classes

+ * + *
    + *
  • TokenType - Enum with hex colors and priorities for all token types
  • + *
  • TypeInfo - Immutable type metadata (class/interface/enum)
  • + *
  • TypeResolver - Class resolution with caching (replaces ClassPathFinder)
  • + *
  • ImportData - Import statement tracking with resolution status
  • + *
  • FieldInfo - Field/variable metadata with scope (global/local/parameter)
  • + *
  • MethodInfo - Method declaration/call metadata with parameters
  • + *
+ * + *

Key Improvements Over Original

+ * + *
    + *
  1. Single-pass tokenization - 7 distinct phases vs multiple loops
  2. + *
  3. Token navigation - prev()/next() methods for easy traversal
  4. + *
  5. Type-specific metadata - Rich information attached to tokens
  6. + *
  7. Hex color support - Direct RGB colors in addition to MC color codes
  8. + *
  9. Clean separation - Each class has one responsibility
  10. + *
  11. Immutable data - TypeInfo, FieldInfo, MethodInfo are immutable
  12. + *
  13. Feature toggle - USE_NEW_INTERPRETER flag for safe rollback
  14. + *
+ * + *

Usage

+ * + *
{@code
+ * // The new system is automatically used via ScriptTextContainer
+ * // To disable and use original JavaTextContainer:
+ * ScriptTextContainer.USE_NEW_INTERPRETER = false;
+ * 
+ * // To access the underlying ScriptDocument:
+ * ScriptTextContainer container = (ScriptTextContainer) textArea.getContainer();
+ * ScriptDocument doc = container.getDocument();
+ * 
+ * // Token navigation example:
+ * for (ScriptLine line : doc.getLines()) {
+ *     for (Token token : line.getTokens()) {
+ *         Token next = token.next();
+ *         Token prev = token.prev();
+ *     }
+ * }
+ * }
+ * + * @since 2.0 + * @see ScriptTextContainer + * @see ScriptDocument + * @see Token + */ +package noppes.npcs.client.gui.util.script.interpreter; From b1799b5a22ea966174deab4957bddfa571e99937 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 23 Dec 2025 00:01:49 +0200 Subject: [PATCH 002/337] Old GuiScriptTextArea --- .../client/gui/util/GuiScriptTextArea1.java | 2186 +++++++++++++++++ .../interpreter/ScriptTextContainer.java | 2 +- 2 files changed, 2187 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java new file mode 100644 index 000000000..fbf439767 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java @@ -0,0 +1,2186 @@ +package noppes.npcs.client.gui.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.util.ChatAllowedCharacters; +import noppes.npcs.NoppesStringUtils; +import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; +import noppes.npcs.client.gui.util.script.*; +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; +import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import noppes.npcs.client.key.impl.ScriptEditorKeys; +import noppes.npcs.util.ValueUtil; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; +import java.util.regex.Matcher; + +import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; + +/** + * Script text editor component with syntax highlighting, bracket matching, + * smooth scrolling, and IDE-like features. + * + * Helper classes used: + * - ScrollState: smooth scroll animation and state management + * - SelectionState: cursor position and text selection management + * - BracketMatcher: bracket matching and brace span computation + * - IndentHelper: indentation utilities and text formatting + * - CommentHandler: line comment toggling + * - CursorNavigation: cursor movement logic + */ +public class GuiScriptTextArea1 extends GuiNpcTextField { + + // ==================== DIMENSIONS & POSITION ==================== + public int x; + public int y; + + // ==================== STATE FLAGS ==================== + public boolean active = false; + public boolean enabled = true; + public boolean visible = true; + public boolean clicked = false; + public boolean doubleClicked = false; + public boolean tripleClicked = false; + private int clickCount = 0; + private long lastClicked = 0L; + + // ==================== TEXT & CONTAINER ==================== + public String text = null; + public String highlightedWord; + private ScriptTextContainer container = null; + private boolean enableCodeHighlighting = false; + // Extra empty lines to allow padding at the bottom of the editor viewport + private int bottomPaddingLines = 6; + + // Search bar layout tracking to allow idempotent/resilient resizing + private int searchBaseY = 0; + private int searchBaseHeight = 0; + private int searchAppliedOffset = 0; + private boolean searchBaseInitialized = false; + + private int getPaddedLineCount() { + if (container == null) return 0; + // Only add bottom padding when the content is already scrollable. This avoids + // introducing a scrollbar when there's nothing to scroll for. + if (container.linesCount > container.visibleLines - bottomPaddingLines) { + return Math.max(0, container.linesCount + bottomPaddingLines); + } else { + return container.linesCount; + } + } + + // ==================== HELPER CLASS INSTANCES ==================== + private final ScrollState scroll = new ScrollState(); + private final SelectionState selection = new SelectionState(); + + // ==================== UI COMPONENTS ==================== + private int cursorCounter; + private ITextChangeListener listener; + private static int LINE_NUMBER_GUTTER_WIDTH = 25; + + // ==================== UNDO/REDO ==================== + public List undoList = new ArrayList<>(); + public List redoList = new ArrayList<>(); + public boolean undoing = false; + + // ==================== KEYS ==================== + public static final ScriptEditorKeys KEYS = new ScriptEditorKeys(); + public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS); + + // ==================== SEARCH/REPLACE ==================== + public static final SearchReplaceBar searchBar = new SearchReplaceBar(); + + // ==================== GO TO LINE ==================== + private final GoToLineDialog goToLineDialog = new GoToLineDialog(); + + // ==================== RENAME REFACTOR ==================== + private final RenameRefactorHandler renameHandler = new RenameRefactorHandler(); + + // ==================== CONSTRUCTOR ==================== + + public GuiScriptTextArea1(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { + super(id, guiScreen, x, y, width, height, null); + init(x, y, width, height, text); + } + + public void init(int x, int y, int width, int height, String text) { + this.x = xPosition = x; + this.y = yPosition = y; + this.width = width; + this.height = height; + this.undoing = true; + this.setText(text); + this.undoing = false; + setCallbacks(); + // Reset search-base tracking whenever the editor is (re)initialized + this.searchBaseY = 0; + this.searchBaseHeight = 0; + this.searchAppliedOffset = 0; + this.searchBaseInitialized = false; + + KEYS_OVERLAY.openOnClick = true; + initGui(); + initializeKeyBindings(); + } + public void initGui() { + int endX = x + width, endY = y + height; + int xOffset = hasVerticalScrollbar() ? -8 : -2; + KEYS_OVERLAY.scale = 0.75f; + KEYS_OVERLAY.borderCol1 = KEYS_OVERLAY.borderCol2 = 0xFF3c3c3c; + int overlayWidth = 160; + KEYS_OVERLAY.initGui(x + (width - overlayWidth) / 2 + 5, y + height / 10, overlayWidth, + height - height / 5 - 10); + + KEYS_OVERLAY.viewButton.scale = 0.45f; + KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); + + // Initialize search bar (preserves state across initGui calls) + searchBar.initGui(x, y, width); + if (searchBar.isVisible()) { // If open + // Shift viewport down again using the bar's current height + searchBar.callback.resizeEditor(true, searchBar.getTotalHeight()); + if (!active) // Focus search if opening another script tab & bar is open + searchBar.focus(false); + } + + // Initialize Go To Line dialog + goToLineDialog.initGui(x, y, width); + } + + public void setCallbacks() { + searchBar.setCallback(new SearchReplaceBar.SearchCallback() { + @Override + public String getText() { + return GuiScriptTextArea1.this.text; + } + + public String getHighlightedWord() { + return GuiScriptTextArea1.this.highlightedWord; + } + + @Override + public int getSelectionStart() { + return GuiScriptTextArea1.this.selection.getStartSelection(); + } + + @Override + public int getSelectionEnd() { + return GuiScriptTextArea1.this.selection.getEndSelection(); + } + + @Override + public void setText(String newText) { + GuiScriptTextArea1.this.setText(newText); + } + + @Override + public void scrollToPosition(int position) { + // Find line containing position and scroll to it + if (container == null || container.lines == null) + return; + + // Calculate offset to account for search bar height + int searchBarOffset = searchBar.getTotalHeight(); + int effectiveHeight = GuiScriptTextArea1.this.height - searchBarOffset; + int visibleLines = effectiveHeight / container.lineHeight; + // Calculate how many lines the search bar covers + int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil( + (double) searchBarOffset / container.lineHeight) : 0; + + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (position >= ld.start && position < ld.end) { + int visible = Math.max(1, visibleLines); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + int targetLine = i; + + // If search bar is visible and would hide this line, scroll down so it's visible + // The target line should appear below the search bar, not under it + int currentScroll = scroll.getScrolledLine(); + int firstVisibleLine = currentScroll + linesHiddenBySRB; + + if (searchBarOffset > 0 && targetLine < firstVisibleLine) { + // Force scroll so target line appears just below the search bar + scroll.setTargetScroll(Math.max(0, targetLine - linesHiddenBySRB), maxScroll); + } else { + scroll.scrollToLine(targetLine, effectiveVisible, maxScroll); + } + break; + } + } + } + + @Override + public void setSelection(int start, int end) { + selection.setSelection(start, end); + selection.setCursorPositionDirect(end); + } + + @Override + public int getGutterWidth() { + return LINE_NUMBER_GUTTER_WIDTH; + } + + @Override + public void unfocusMainEditor() { + // Save position but unfocus + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + searchBar.resetSelection(); + } + + @Override + public void onMatchesUpdated() { + // Called when matches change - could be used for UI updates + } + + // Robust resize: use base editor bounds and apply the requested offset + public void resizeEditor(boolean open, int barHeight) { + int desiredOffset = Math.max(0, barHeight); + // Initialize base values if not set + if (!searchBaseInitialized) { + searchBaseY = GuiScriptTextArea1.this.y; + searchBaseHeight = GuiScriptTextArea1.this.height; + searchAppliedOffset = 0; + searchBaseInitialized = true; + } + + int targetOffset = open ? desiredOffset : 0; + if (targetOffset == searchAppliedOffset) + return; // already in desired state + + // Compute new bounds from base values (idempotent) + int newY = searchBaseY + targetOffset; + int newHeight = Math.max(12, searchBaseHeight - targetOffset); + + GuiScriptTextArea1.this.y = newY; + GuiScriptTextArea1.this.height = newHeight; + searchAppliedOffset = targetOffset; + + if (container != null) + container.visibleLines = Math.max(GuiScriptTextArea1.this.height / container.lineHeight - 1, 1); + } + }); + // Initialize Go To Line dialog with callback + goToLineDialog.setCallback(new GoToLineDialog.GoToLineCallback() { + @Override + public int getLineCount() { + return container != null ? container.linesCount : 0; + } + + @Override + public int getColumnCount(int lineIndex) { + if (container == null || container.lines == null || lineIndex < 0 || lineIndex >= container.lines.size()) { + return 0; + } + LineData ld = container.lines.get(lineIndex); + return ld.end - ld.start; + } + + @Override + public void goToLineColumn(int line, int column) { + if (container == null || container.lines == null) + return; + + // Convert 1-indexed line to 0-indexed + int lineIdx = line - 1; + if (lineIdx < 0 || lineIdx >= container.lines.size()) + return; + + LineData ld = container.lines.get(lineIdx); + int lineLength = ld.end - ld.start; + + // Convert 1-indexed column to 0-indexed, clamp to line length + // lineLength - 1 because the line's end is the start of the next line + int col = Math.max(0, Math.min(column - 1, lineLength - 1)); + int position = ld.start + col; + + // Set cursor position + selection.reset(position); + + // Scroll to make the line visible + int visible = GuiScriptTextArea1.this.height / (container != null ? container.lineHeight : 12); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll); + } + + @Override + public void unfocusMainEditor() { + // Save position but unfocus + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } + + @Override + public void onDialogClose() { + active = true; + selection.markActivity(); + } + }); + + // Initialize Rename Refactor handler with callback + renameHandler.setCallback(new RenameRefactorHandler.RenameCallback() { + @Override + public String getText() { + return GuiScriptTextArea1.this.text; + } + + @Override + public void setText(String newText) { + GuiScriptTextArea1.this.setText(newText); + } + + @Override + public List getLines() { + return container != null ? container.lines : new ArrayList<>(); + } + + @Override + public int getCursorPosition() { + return selection.getCursorPosition(); + } + + public SelectionState getSelectionState() { + return selection; + } + + @Override + public void setCursorPosition(int pos) { + selection.reset(pos); + } + + @Override + public void unfocusMainEditor() { + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } + + @Override + public int getGutterWidth() { + return LINE_NUMBER_GUTTER_WIDTH; + } + + @Override + public int getLineHeight() { + return container != null ? container.lineHeight : 12; + } + + @Override + public int getScrolledLine() { + return scroll.getScrolledLine(); + } + + @Override + public double getFractionalOffset() { + return scroll.getFractionalOffset(); + } + + @Override + public void scrollToPosition(int pos) { + if (container == null || container.lines == null) + return; + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (pos >= ld.start && pos < ld.end) { + int visible = Math.max(1, container.visibleLines); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + scroll.scrollToLine(i, effectiveVisible, maxScroll); + break; + } + } + } + + @Override + public JavaTextContainer getContainer() { + return container; + } + + @Override + public void setTextWithoutUndo(String newText) { + // Set text without creating an undo entry (for live rename preview) + boolean wasUndoing = undoing; + undoing = true; + setText(newText); + undoing = wasUndoing; + } + + @Override + public void pushUndoState(String textState, int cursor) { + // Push a specific text state to the undo list + if (!undoing) { + undoList.add(new UndoData(textState, cursor)); + redoList.clear(); + } + } + + @Override + public int getViewportWidth() { + return width - LINE_NUMBER_GUTTER_WIDTH - 8; // Account for gutter and scrollbar + } + }); + } + + public boolean fullscreen() { + return GuiScriptInterface.isFullscreen; + } + // ==================== RENDERING ==================== + public void drawTextBox(int xMouse, int yMouse) { + if (!visible) + return; + clampSelectionBounds(); + + // Dynamically calculate gutter width based on line count digits + if (container != null && container.linesCount > 0) { + int maxLineNum = container.linesCount; + String maxLineStr = String.valueOf(maxLineNum); + int digitWidth = ClientProxy.Font.width(maxLineStr); + LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right) + } + // Draw outer border around entire area + int offset = fullscreen() ? 2 : 1; + drawRect(x - offset, y - offset - searchBar.getTotalHeight(), x + width + offset, y + height + offset, + 0xffa0a0a0); + + int searchHeight = searchBar.getTotalHeight(); + + + // Draw line number gutter background + drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000); + // Draw text viewport background (starts after gutter) + drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000); + // Draw separator line between gutter and text area + drawRect(x + LINE_NUMBER_GUTTER_WIDTH-1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41); + + // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) + GL11.glEnable(GL11.GL_SCISSOR_TEST); + scissorViewport(); + + container.visibleLines = (height / container.lineHeight); + + int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); + + // Handle mouse wheel scroll + int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); + if (listener instanceof GuiNPCInterface) { + ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; + if (wheelDelta != 0 && canScroll) + scroll.applyWheelScroll(wheelDelta, maxScroll); + } + + // Handle scrollbar dragging (delegated to ScrollState) + if (scroll.isClickScrolling()) + scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, getPaddedLineCount(), maxScroll); + + // Update scroll animation + scroll.initializeIfNeeded(scroll.getScrolledLine()); + scroll.update(maxScroll); + + // Handle click-dragging for selection + if (clicked) { + clicked = Mouse.isButtonDown(0); + int i = getSelectionPos(xMouse, yMouse); + if (i != selection.getCursorPosition()) { + if (doubleClicked || tripleClicked) { + selection.reset(selection.getCursorPosition()); + doubleClicked = false; + tripleClicked = false; + } + setCursor(i, true); + } + } else if (doubleClicked || tripleClicked) { + doubleClicked = false; + tripleClicked = false; + } + // y += searchHeight; + // height -= searchHeight; + // Calculate braces next to cursor to highlight + int startBracket = 0, endBracket = 0; + if (selection.getStartSelection() >= 0 && text != null && text.length() > 0 && + (selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) { + int[] span = BracketMatcher.findBracketSpanAt(text,selection.getStartSelection()); + if (span != null) { + startBracket = span[0]; + endBracket = span[1]; + } + } + + List list = new ArrayList<>(container.lines); + + // Build brace spans: {origDepth, open line, close line, adjustedDepth} + List braceSpans = BracketMatcher.computeBraceSpans(text, list); + // Always highlight unmatched braces (positions in text) + List unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text); + + // Determine which exact brace span (openLine/closeLine) to highlight indent guides for + int highlightedOpenLine = -1; + int highlightedCloseLine = -1; + if (startBracket != endBracket && startBracket >= 0) { + int bracketLineIdx = -1; + // Only consider curly braces for highlighting the indent guides + boolean isCurlyBracket = false; + char bc = text.charAt(startBracket); + if (text != null && startBracket >= 0 && startBracket < text.length()) { + if (bc == '{' || bc == '}') isCurlyBracket = true; + } + for (int li = 0; li < list.size(); li++) { + LineData ld = list.get(li); + if (startBracket >= ld.start && startBracket < ld.end) { + bracketLineIdx = li; + break; + } + } + if (bracketLineIdx >= 0 && isCurlyBracket) { + // Prefer a span that directly matches the bracket character's line: + // - if the bracket is an opening '{', prefer spans where openLine == bracketLineIdx + // - if the bracket is a closing '}', prefer spans where closeLine == bracketLineIdx + // If no exact match is found, fall back to the smallest enclosing span (innermost). + int bestSize = Integer.MAX_VALUE; + boolean foundExact = false; + char bracketChar = bc; // from earlier + for (int[] span : braceSpans) { + int openLine = span[1]; + int closeLine = span[2]; + if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) { + int size = closeLine - openLine; + boolean exactMatch = (bracketChar == '{' && openLine == bracketLineIdx) || (bracketChar == '}' && closeLine == bracketLineIdx); + if (exactMatch) { + // Prefer exact matches immediately (still choose smallest exact span) + if (!foundExact || size < bestSize) { + foundExact = true; + bestSize = size; + highlightedOpenLine = openLine; + highlightedCloseLine = closeLine; + } + } else if (!foundExact) { + // keep the smallest enclosing span as a fallback + if (size < bestSize) { + bestSize = size; + highlightedOpenLine = openLine; + highlightedCloseLine = closeLine; + } + } + } + } + } + } + + highlightedWord = null; + if (selection.hasSelection()) { + Matcher m = container.regexWord.matcher(text); + while (m.find()) { + if (m.start() == selection.getStartSelection() && m.end() == selection.getEndSelection()) { + highlightedWord = text.substring(selection.getStartSelection(), selection.getEndSelection()); + } + } + } + + // Apply fractional GL translate for sub-pixel smooth scrolling + double fracOffset = scroll.getFractionalOffset(); + float fracPixels = (float) (fracOffset * container.lineHeight); + GL11.glPushMatrix(); + GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + + + // Expand render range by one line above/below so partially-visible lines are drawn + int renderStart = Math.max(0, scroll.getScrolledLine() - 1); + // Compute the last line index to render, including the last partially-visible line if any. + // Adds the fractional scroll offset (fracOffset * lineHeight) to ensure the bottom line is drawn + // when only part of it is visible in the viewport. + int renderEnd = (int) Math.min(list.size() - 1, + scroll.getScrolledLine() + container.visibleLines + fracPixels + 1); + + // Strings start drawing vertically this much into the line. + int stringYOffset = 2; + + // Render LINE GUTTER numbers + for (int i = renderStart; i <= renderEnd; i++) { + int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; + String lineNum = String.valueOf(i + 1); + int lineNumWidth = ClientProxy.Font.width(lineNum); + int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding + int lineNumY = posY + 1; + // Highlight current line number + int lineNumColor = 0xFF606366; + if (active && isEnabled()) { + for (int li = 0; li < list.size(); li++) { + LineData ld = list.get(li); + if (selection.getCursorPosition() >= ld.start && selection.getCursorPosition() < ld.end || (li == list.size() - 1 && selection.getCursorPosition() == text.length())) { + if (li == i) { + lineNumColor = 0xFFb9c7d6; + break; + } + } + } + } + ClientProxy.Font.drawString(lineNum, lineNumX, lineNumY, lineNumColor); + } + + // Render Viewport + for (int i = renderStart; i <= renderEnd; i++) { + LineData data = list.get(i); + ScriptLine scriptLine = container.getDocument().getLineAt(i); + String line = data.text; + int w = line.length(); + // Use integer Y relative to scrolledLine; fractional offset applied via GL translate + int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight; + if (i >= renderStart && i <= renderEnd) { + //Highlight braces the cursor position is on + if (startBracket != endBracket) { + if (startBracket >= data.start && startBracket < data.end) { + int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); + int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); + } + if (endBracket >= data.start && endBracket < data.end) { + int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); + int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); + } + } + + // Highlight unmatched braces in this line (always red) + if (unmatchedBraces != null && !unmatchedBraces.isEmpty()) { + for (int ubPos : unmatchedBraces) { + if (ubPos >= data.start && ubPos < data.end) { + int rel = ubPos - data.start; + int s = ClientProxy.Font.width(line.substring(0, rel)); + int e = ClientProxy.Font.width(line.substring(0, rel + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0xffcc0000); + } + } + } + //Highlight words + if (highlightedWord != null) { + Matcher m = container.regexWord.matcher(line); + while (m.find()) { + if (line.substring(m.start(), m.end()).equals(highlightedWord)) { + int s = ClientProxy.Font.width(line.substring(0, m.start())); + int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x99004c00); + } + } + } + + // Highlight search matches + if (searchBar.isVisible()) { + List searchMatches = searchBar.getMatches(); + int currentMatchIdx = searchBar.getCurrentMatchIndex(); + for (int mi = 0; mi < searchMatches.size(); mi++) { + int[] match = searchMatches.get(mi); + // Check if match overlaps with this line + if (match[1] > data.start && match[0] < data.end) { + int matchStart = Math.max(match[0] - data.start, 0); + int matchEnd = Math.min(match[1] - data.start, line.length()); + if (matchStart < matchEnd) { + int s = ClientProxy.Font.width(line.substring(0, matchStart)); + int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1; + boolean isExcluded = searchBar.isMatchExcluded(mi); + // Current match gets brighter highlight, others get dimmer + int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa; + if (isExcluded) { + highlightColor = 0x33666666; // Dimmer for excluded matches + } + int highlightX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; + int highlightEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; + drawRect(highlightX, posY, highlightEndX, posY + container.lineHeight, highlightColor); + + // Draw strikethrough line for excluded matches + if (isExcluded) { + int strikeY = posY + container.lineHeight / 2; + drawRect(highlightX, strikeY, highlightEndX, strikeY + 1, 0xFFaa4444); + } + } + } + } + } + + // Highlight rename refactor occurrences + if (renameHandler.isActive()) { + List renameOccurrences = renameHandler.getOccurrences(); + for (int[] occ : renameOccurrences) { + // Check if occurrence overlaps with this line + if (occ[1] >= data.start && occ[0] <= data.end) { + int occStart = Math.max(occ[0] - data.start, 0); + int occEnd = Math.min(occ[1] - data.start, line.length()); + + // Handle empty word case - draw 1 pixel wide box + boolean isEmpty = (occ[0] == occ[1]); + + if (occStart <= occEnd) { // Changed from < to <= to handle empty case + int s = ClientProxy.Font.width(line.substring(0, occStart)); + int e = isEmpty ? s + 2 : ClientProxy.Font.width( + line.substring(0, occEnd)) + 1; // 2px wide for empty + int occX = x + LINE_NUMBER_GUTTER_WIDTH + s; + int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 2 + e; + boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]); + + // Draw background highlight + int bgColor = isPrimary ? 0x55335577 : 0x33224466; + drawRect(occX, posY, occEndX, posY + container.lineHeight, bgColor); + + // Draw white border for primary occurrence (IntelliJ-like) + if (isPrimary) { + int borderColor = 0xDDFFFFFF; + //RED BG + drawRect(occX, posY, occEndX, posY + container.lineHeight, 0x33ff0000); + + // Top border + drawRect(occX, posY, occEndX, posY + 1, borderColor); + // Bottom border + drawRect(occX, posY + container.lineHeight - 1, occEndX, + posY + container.lineHeight, borderColor); + // Left border + drawRect(occX, posY, occX + 1, posY + container.lineHeight, borderColor); + // Right border + drawRect(occEndX - 1, posY, occEndX, posY + container.lineHeight, borderColor); + + // Draw cursor inside the primary occurrence + if (renameHandler.shouldShowCursor()) { + int cursorInWord = renameHandler.getCursorInWord(); + String currentWord = renameHandler.getCurrentWord(); + if (currentWord != null && cursorInWord >= 0 && cursorInWord <= currentWord.length()) { + String beforeCursor = currentWord.substring(0, + Math.min(cursorInWord, currentWord.length())); + int cursorX = occX + ClientProxy.Font.width(beforeCursor); + // drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1, + // 0xFFFFFFFF); + } + } + } + } + } + } + } + + // Highlight the current line (light gray) under any selection + if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { + drawRect(x , posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); + } + // Highlight selection + if (selection.hasSelection() && selection.getEndSelection() > data.start && selection.getStartSelection() <= data.end) { + if (selection.getStartSelection() < data.end) { + int s = ClientProxy.Font.width( + line.substring(0, Math.max(selection.getStartSelection() - data.start, 0))); + int e = ClientProxy.Font.width( + line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); + } + } + + // Draw indent guides once per visible block based on brace spans + if (i == Math.max(0, scroll.getScrolledLine()) && !braceSpans.isEmpty()) { + int visStart = Math.max(0, scroll.getScrolledLine()); + int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 0); + for (int[] span : braceSpans) { + int originalDepth = span[0]; + int openLine = span[1]; + int closeLine = span[2]; + int depth = span.length > 3 ? span[3] : originalDepth; + // Skip top-level (depth 1) using the original depth to avoid hiding nested guides when adjusted + if (originalDepth <= 1) + continue; + int startLine = openLine + 1; // start under the opening brace + int endLine = closeLine - 1; // stop before the closing brace + if (startLine > endLine) + continue; + if (endLine < visStart || startLine > visEnd) + continue; + + int drawStart = Math.max(startLine, visStart); + int drawEnd = Math.min(endLine, visEnd); + // Compute horizontal position: 4 spaces per indent level, minus a tiny left offset + int safeDepth = Math.max(1, depth); + int spaces = (safeDepth - 1) * 4; + StringBuilder sb = new StringBuilder(); + for (int k = 0; k < spaces; k++) + sb.append(' '); + int px = ClientProxy.Font.width(sb.toString()); + int gx = x + LINE_NUMBER_GUTTER_WIDTH + 4 + px - 2; // shift left ~2px for the IntelliJ feel + + boolean highlighted = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine); + int guideColor = highlighted ? 0x9933cc00 : 0x33FFFFFF; + + int topY = y + (drawStart - scroll.getScrolledLine()) * container.lineHeight; + int bottomY = y + (endLine - scroll.getScrolledLine() + 1) * container.lineHeight; + if(highlighted) + bottomY-=2; + drawRect(gx, topY, gx + 1, bottomY, guideColor); + } + } + int yPos = posY + stringYOffset; + + // data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + scriptLine.drawString(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + + // Draw cursor: pause blinking while user is active recently + boolean recentInput = selection.hadRecentInput(); + if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { + int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width( + line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); + drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff); + } + } + } + GL11.glPopMatrix(); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + + if (hasVerticalScrollbar()) { + Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource); + int effLines = Math.max(1, getPaddedLineCount()); + int sbSize = Math.max((int) (1f * (container.visibleLines) / effLines * height), 2); + + int posX = x + width - 6; + double linesCount = (double) effLines; + int posY = (int) (y + 1f * scroll.getScrollPos() / linesCount * (height - 4)) + 1; + + drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); + } + + // Draw search/replace bar (overlays viewport) + searchBar.draw(xMouse, yMouse); + + // Draw go to line dialog (overlays everything) + goToLineDialog.draw(xMouse, yMouse); + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); + } + + private void scissorViewport() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + int scissorX = (this.x) * scaleFactor; + int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor; + int scissorW = (this.width) * scaleFactor; + int scissorH = this.height * scaleFactor; + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + } + // ==================== SELECTION & CURSOR POSITION ==================== + + // Get cursor position from mouse coordinates + private int getSelectionPos(int xMouse, int yMouse) { + xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); + yMouse -= this.y + 1; + // Adjust yMouse to account for fractional GL translation (negative offset applied in rendering). + // Use a double here (no integer rounding) so clicks that land on partially + // visible lines (fractional positions) correctly hit that line. + double fracPixels = scroll.getFractionalOffset() * container.lineHeight; + double yMouseD = yMouse + fracPixels; + + ArrayList list = new ArrayList(this.container.lines); + + for (int i = 0; i < list.size(); ++i) { + LineData data = (LineData) list.get(i); + //+1 to account for the fractional line + if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines +1) { + double yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight; + if (yMouseD >= yPos && yMouseD < yPos + this.container.lineHeight) { + int lineWidth = 0; + char[] chars = data.text.toCharArray(); + + for (int j = 1; j <= chars.length; ++j) { + int w = ClientProxy.Font.width(data.text.substring(0, j)); + if (xMouse < lineWidth + (w - lineWidth) / 2) { + return data.start + j - 1; + } + + lineWidth = w; + } + + // Place cursor after the last visible character of the line. + // `data.end - 1` previously pointed at the newline for non-last lines + // which made clicks land on the newline rather than after the text. + // Use data.start + chars.length to return the position directly + // after the line's characters, clamped to the total text length. + int posAfterChars = data.start + chars.length; + return Math.min(posAfterChars, text.length()); + } + } + } + + return this.container.text.length(); + } + + // Find which line the cursor is on (0-indexed) + private int getCursorLineIndex() { + return selection.getCursorLineIndex(container.lines, text != null ? text.length() : 0); + } + + + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) + // Only scrolls if cursor is outside the visible area + private void scrollToCursor() { + if (container == null || container.lines == null || container.lines.isEmpty()) return; + + int lineIdx = getCursorLineIndex(); + int visible = Math.max(1, container.visibleLines); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + + scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll); + } + + // ==================== KEY BINDINGS INITIALIZATION ==================== + + /** + * Initialize key bindings for editor shortcuts using ScriptEditorKeys. + * Centralized active/enabled checking ensures shortcuts only fire when appropriate. + */ + private void initializeKeyBindings() { + // Helper: execute action only if text area is active and enabled + Supplier isActive = () -> active && isEnabled() && !KEYS_OVERLAY.isVisible(); + Supplier openBoxes = () -> !KEYS_OVERLAY.isVisible(); + + // CUT: Copy selection to clipboard and delete it. If no selection, cut the current sentence. + KEYS.CUT.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(s.length()); + scrollToCursor(); + return; + } + + // No selection: cut the current sentence (heuristic based on .!? or newline) + if (text == null || text.isEmpty()) + return; + int cursor = selection.getCursorPosition(); + int start = cursor; + int pos = cursor - 1; + while (pos >= 0) { + char ch = text.charAt(pos); + if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') { + start = pos + 1; + break; + } + pos--; + } + while (start < cursor && Character.isWhitespace(text.charAt(start))) start++; + + int end = cursor; + pos = cursor; + while (pos < text.length()) { + char ch = text.charAt(pos); + if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') { + end = pos + 1; + break; + } + pos++; + } + if (end == cursor) end = text.length(); + while (end > start && Character.isWhitespace(text.charAt(end - 1))) end--; + + if (start >= end) { + // fallback: cut whole current line + int ls = text.lastIndexOf('\n', Math.max(0, cursor - 1)); + start = ls == -1 ? 0 : ls + 1; + int le = text.indexOf('\n', cursor); + end = le == -1 ? text.length() : le; + } + + if (start < end) { + String cut = text.substring(start, end); + NoppesStringUtils.setClipboardContents(cut); + setText(text.substring(0, start) + text.substring(end)); + selection.reset(start); + scrollToCursor(); + } + }); + + // COPY: Copy selection to clipboard + KEYS.COPY.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + }); + + // PASTE: Insert clipboard contents at caret + KEYS.PASTE.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); + }); + + // UNDO: Restore last edit from undo list + // Works in search bar. + KEYS.UNDO.setTask(e -> { + if ((!e.isPress() && !e.isHold()) || !openBoxes.get()) + return; + + if (searchBar.hasFocus()) { + searchBar.undo(); + } else { + if (undoList.isEmpty()) + return; + undoing = true; + redoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = undoList.remove(undoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + searchBar.updateMatches(); + if (!active) + active = true; + } + }); + + // REDO: Restore last undone edit from redo list + // Works in search bar. + KEYS.REDO.setTask(e -> { + if ((!e.isPress() && !e.isHold()) || !openBoxes.get()) + return; + + if (searchBar.hasFocus()) { + searchBar.redo(); + } else { + if (redoList.isEmpty()) + return; + undoing = true; + undoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = redoList.remove(redoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + searchBar.updateMatches(); + if (!active) + active = true; + } + }); + + // FORMAT: Format/indent code + KEYS.FORMAT.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + formatText(); + }); + + // TOGGLE_COMMENT: Toggle comment for selection or current line + KEYS.TOGGLE_COMMENT.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) + toggleCommentSelection(); + else + toggleCommentLineAtCursor(); + }); + + // DUPLICATE: Duplicate selection or current line + KEYS.DUPLICATE.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (selection.hasSelection()) { + // Multi-line selection duplication + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) + firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + selectedText + text.substring(insertAt)); + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + } + } else { + // Duplicate current line + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + int safeStart = Math.max(0, Math.min(line.start, text.length())); + int safeEnd = Math.max(safeStart, Math.min(line.end, text.length())); + String lineText = text.substring(safeStart, safeEnd); + String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText; + int insertionPoint = Math.min(line.end, text.length()); + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + break; + } + } + } + }); + + + // Check if can open just for SearchReplaceBar and GoToLine + + // SEARCH: Open search bar (Ctrl+R) + // Works in search bar. + KEYS.SEARCH.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + unfocusAll(); + searchBar.openSearch(); + }); + + // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) + // Works in search bar. + KEYS.SEARCH_REPLACE.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + unfocusAll(); + searchBar.openSearchReplace(); + }); + + // GO_TO_LINE: Open go to line dialog (Ctrl+G) + // Works in search bar. + KEYS.GO_TO_LINE.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + unfocusAll(); + goToLineDialog.toggle(); + }); + + // RENAME: Start rename refactoring (Shift+F6) + KEYS.RENAME.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + + if (!renameHandler.isActive()) { + unfocusAll(); + active = true; + renameHandler.startRename(); + } + }); + } + + public void unfocusAll() { + if (searchBar.hasFocus()) searchBar.unfocus(); + if (goToLineDialog.hasFocus()) goToLineDialog.unfocus(); + if (renameHandler.isActive()) + renameHandler.cancel(); + } + // ==================== KEYBOARD INPUT HANDLING ==================== + + /** + * Handles keyboard input for the text area, delegating to specialized handlers + * for different types of input: navigation, deletion, shortcuts, and character input. + */ + @Override + public boolean textboxKeyTyped(char c, int i) { + if (KEYS_OVERLAY.keyTyped(c, i)) + return true; + + // Handle rename refactor input first if active + if (renameHandler.isActive() && renameHandler.keyTyped(c, i)) + return true; + + // Handle Go To Line dialog input first if it has focus + if (goToLineDialog.isVisible() &&goToLineDialog.keyTyped(c, i)) + return true; + + // Handle search bar input first if it has focus + if (searchBar.isVisible() && searchBar.keyTyped(c, i)) + return true; + + if (!active) + return false; + + if (this.isKeyComboCtrlA(i)) { + selection.selectAll(text.length()); + return true; + } + + if (!isEnabled()) return false; + + if (handleNavigationKeys(i)) return true; + if (handleInsertionKeys(i)) return true; + if (handleDeletionKeys(i)) return true; + if (handleCharacterInput(c)) return true; + + return true; + } + + /** + * Handles cursor navigation keys (arrows) with support for word-jumping (Ctrl) + * and selection (Shift). Updates cursor position and scrolls viewport if needed. + */ + private boolean handleNavigationKeys(int i) { + // LEFT ARROW: move cursor left; with Ctrl -> jump by word + if (i == Keyboard.KEY_LEFT) { + int j = 1; // default: move one character + if (isCtrlKeyDown()) { + // When Ctrl is down, compute distance to previous word boundary. + // We match words in the text slice before the cursor and pick + // the last match start as the new boundary. + Matcher m = container.regexWord.matcher(text.substring(0, selection.getCursorPosition())); + while (m.find()) { + if (m.start() == m.end()) + continue; // skip empty matches + // j becomes the number of chars to move left to reach word start + j = selection.getCursorPosition() - m.start(); + } + } + int newPos = Math.max(selection.getCursorPosition() - j, 0); + // If Shift is held, extend selection; otherwise place caret. + setCursor(newPos, GuiScreen.isShiftKeyDown()); + return true; + } + + // RIGHT ARROW: move cursor right; with Ctrl -> jump to next word start + if (i == Keyboard.KEY_RIGHT) { + int j = 1; // default: move one character + if (isCtrlKeyDown()) { + String after = text.substring(selection.getCursorPosition()); + Matcher m = container.regexWord.matcher(after); + if (m.find()) { + if (m.start() == 0) { + // If the first match starts at 0 (cursor at word start), + // try to find the next match so we advance past the current word. + if (m.find()) + j = m.start(); + else + j = Math.max(1, after.length()); + } else { + j = m.start(); + } + } else { + // No word match found after cursor -> jump to end + j = Math.max(1, after.length()); + } + } + int newPos = Math.min(selection.getCursorPosition() + j, text.length()); + setCursor(newPos, GuiScreen.isShiftKeyDown()); + return true; + } + + // UP/DOWN: logical cursor movement across lines while preserving + // column where possible. After moving, ensure the caret remains visible + // by adjusting the scroll if necessary. + if (i == Keyboard.KEY_UP) { + setCursor(cursorUp(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); + return true; + } + if (i == Keyboard.KEY_DOWN) { + setCursor(cursorDown(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); + return true; + } + + return false; // not a navigation key + } + + /** + * Handles insertion-related keys such as Tab indentation and Enter behavior. + */ + private boolean handleInsertionKeys(int i) { + // TAB: indent or unindent depending on Shift + if (i == Keyboard.KEY_TAB) { + boolean shift = isShiftKeyDown(); + if (shift) { + handleShiftTab(); + } else { + handleTab(); + } + scrollToCursor(); + return true; + } + + // RETURN/ENTER: special handling when preceding char is an opening brace '{' + if (i == Keyboard.KEY_RETURN) { + int cursorPos = selection.getCursorPosition(); + int prevNonWs = cursorPos - 1; + while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace( + text.charAt(prevNonWs))) { + prevNonWs--; + } + + if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') { + String indent = ""; + for (LineData ld : this.container.lines) { + if (prevNonWs >= ld.start && prevNonWs < ld.end) { + indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); + break; + } + } + if (indent == null) + indent = ""; + String childIndent = indent + " "; + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + int firstNewline = after.indexOf('\n'); + String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); + if (leadingSegment.trim().length() > 0) { + addText("\n" + childIndent); + scrollToCursor(); + return true; + } + + boolean hasMatchingCloseSameIndent = false; + try { + int openLineIdx = -1; + int bracePos = prevNonWs; + for (int li = 0; li < this.container.lines.size(); li++) { + LineData ld = this.container.lines.get(li); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = li; + break; + } + } + + if (openLineIdx >= 0) { + List spans = BracketMatcher.computeBraceSpans(text, this.container.lines); + for (int[] span : spans) { + int spanOpen = span[1]; + int spanClose = span[2]; + if (spanOpen == openLineIdx) { + int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); + if (closeIndent == indent.length()) { + hasMatchingCloseSameIndent = true; + } + break; + } + } + } + } catch (Exception ex) { + hasMatchingCloseSameIndent = false; + } + + if (hasMatchingCloseSameIndent) { + addText("\n" + childIndent); + scrollToCursor(); + } else { + String insert = "\n" + childIndent + "\n" + indent + "}"; + setText(before + insert + after); + int newCursor = before.length() + 1 + childIndent.length(); + selection.reset(newCursor); + scrollToCursor(); + } + } else { + addText(Character.toString('\n') + getAutoIndentForEnter()); + scrollToCursor(); + } + return true; + } + + return false; + } + + /** + * Handles deletion keys: Delete, Backspace, and Ctrl+Backspace. + * Includes smart backspace behavior for indentation-aware line merging, + * auto-pair deletion for brackets/quotes, and word-level deletion. + */ + private boolean handleDeletionKeys(int i) { + // DELETE key: remove the character under the cursor if no selection, + // otherwise remove the selected region's tail. + if (i == Keyboard.KEY_DELETE) { + String s = getSelectionAfterText(); + if (!s.isEmpty() && !selection.hasSelection()) + // remove single character after caret when nothing is selected + s = s.substring(1); + setText(getSelectionBeforeText() + s); + // Keep caret at same start selection + selection.reset(selection.getStartSelection()); + return true; + } + + // CTRL+BACKSPACE: delete to previous word or whitespace boundary. + if (isKeyComboCtrlBackspace(i)) { + String s = getSelectionBeforeText(); + if (selection.getStartSelection() > 0 && !selection.hasSelection()) { + int nearestCondition = selection.getCursorPosition(); + int g; + // If the char left of caret is whitespace, find the first non-space to the left; + // otherwise find first whitespace/newline to the left (word boundary). + boolean cursorInWhitespace = Character.isWhitespace(s.charAt(selection.getCursorPosition() - 1)); + if (cursorInWhitespace) { + // Scan left until non-whitespace (start of previous word) + for (g = selection.getCursorPosition() - 1; g >= 0; g--) { + char currentChar = s.charAt(g); + if (!Character.isWhitespace(currentChar)) { + nearestCondition = g; + break; + } + if (g == 0) { + nearestCondition = 0; + } + } + } else { + // Scan left until whitespace/newline is found (word boundary) + for (g = selection.getCursorPosition() - 1; g >= 0; g--) { + char currentChar = s.charAt(g); + if (Character.isWhitespace(currentChar) || currentChar == '\n') { + nearestCondition = g; + break; + } + if (g == 0) { + nearestCondition = 0; + } + } + } + + // Trim the prefix up to the discovered boundary + s = s.substring(0, nearestCondition); + // Adjust selection start to match removed characters + selection.setStartSelection( + selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); + } + setText(s + getSelectionAfterText()); + selection.reset(selection.getStartSelection()); + return true; + } + + // BACKSPACE: complex handling with a few cases: + // 1) If a selection exists, delete it + // 2) If at start, nothing to do + // 3) Smart indent-aware merge with previous line when caret is at/near expected indent + // 4) Auto-pair deletion (remove both opening and closing chars when deleting an opener) + // 5) Fallback: delete a single char to the left + if (i == Keyboard.KEY_BACK) { + // 1) selection deletion + if (selection.hasSelection()) { + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(selection.getStartSelection()); + scrollToCursor(); + return true; + } + + // 2) nothing to delete + if (selection.getStartSelection() <= 0) { + return true; + } + + // If the current line is whitespace-only, delete the whole line + // (including the trailing newline if present). This makes Backspace + // intuitive on blank/indented lines outside any recognized scope. + LineData currCheck = selection.findCurrentLine(container.lines); + if (currCheck != null && currCheck.text.trim().length() == 0) { + int removeEnd = text.indexOf('\n', currCheck.start - 1); + if (removeEnd == -1) { + removeEnd = text.length(); + } else { + removeEnd = removeEnd + 1; // include the newline + } + String before = text.substring(0, ValueUtil.clamp(currCheck.start - 1, 0, text.length())); + String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; + setText(before + after); + int newCursor = Math.max(0, currCheck.start - 1); + selection.reset(newCursor); + scrollToCursor(); + return true; + } + + // 3) indent-aware merge: find current line and compute expected indent + LineData curr = selection.findCurrentLine(container.lines); + if (curr != null && curr.start > 0) { + int col = selection.getCursorPosition() - curr.start; + int actualIndent = IndentHelper.getLineIndent(curr.text); + int expectedIndent = IndentHelper.getExpectedIndent(curr, container.lines); + + // Trigger smart merge only when caret is at or before the expected indent. + if (col <= expectedIndent) { + boolean lineHasContent = curr.text.trim().length() > 0; + int newlinePos = curr.start - 1; // index of newline before this line + + if (!lineHasContent) { + // Empty or whitespace-only line: remove it including its trailing newline + int removeEnd = text.indexOf('\n', curr.start); + if (removeEnd == -1) { + removeEnd = text.length(); + } else { + removeEnd = removeEnd + 1; // include the newline in removal + } + String before = text.substring(0, curr.start); + String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; + setText(before + after); + // Place caret at end of previous line + int newCursor = Math.max(0, curr.start - 1); + selection.reset(newCursor); + scrollToCursor(); + return true; + } else { + // Merge current line content with the previous line preserving spacing + int contentStart = curr.start + actualIndent; + String before = newlinePos >= 0 ? text.substring(0, newlinePos) : ""; + String content = contentStart <= text.length() ? text.substring(contentStart) : ""; + + // Decide whether a space is needed between concatenated fragments. + String spacer = ""; + if (before.length() > 0 && content.length() > 0) { + char lastChar = before.charAt(before.length() - 1); + char firstChar = content.charAt(0); + // Avoid adding space when punctuation/brackets are adjacent + if (!Character.isWhitespace(lastChar) && + lastChar != '{' && lastChar != '(' && lastChar != '[' && + firstChar != '}' && firstChar != ')' && firstChar != ']' && + firstChar != ';' && firstChar != ',' && firstChar != '.' && + firstChar != '\n') { + spacer = " "; + } + } + + setText(before + spacer + content); + int newCursor = before.length() + spacer.length(); + selection.reset(newCursor); + scrollToCursor(); + return true; + } + } + } + + // 4) Auto-pair deletion: when deleting an opener and a matching closer follows, + // remove both so the pair is cleaned up in one backspace. + if (selection.getStartSelection() > 0 && selection.getStartSelection() < text.length()) { + char prev = text.charAt(selection.getStartSelection() - 1); + char nextc = text.charAt(selection.getStartSelection()); + if ((prev == '(' && nextc == ')') || + (prev == '[' && nextc == ']') || + (prev == '{' && nextc == '}') || + (prev == '\'' && nextc == '\'') || + (prev == '"' && nextc == '"')) { + String before = text.substring(0, selection.getStartSelection() - 1); + String after = selection.getStartSelection() + 1 < text.length() ? text.substring( + selection.getStartSelection() + 1) : ""; + setText(before + after); + selection.setStartSelection(selection.getStartSelection() - 1); + selection.reset(selection.getStartSelection()); + scrollToCursor(); + return true; + } + } + + // 5) Normal single-character backspace + String s = getSelectionBeforeText(); + s = s.substring(0, s.length() - 1); + selection.setStartSelection(selection.getStartSelection() - 1); + setText(s + getSelectionAfterText()); + selection.reset(selection.getStartSelection()); + scrollToCursor(); + return true; + } + + return false; + } + + /** + * Handles keyboard shortcuts: clipboard operations (cut/copy/paste), + * undo/redo, tab indentation, code formatting, enter with brace handling, + * comment toggling, and line duplication. + */ + private boolean handleShortcutKeys(int i) { + // CTRL+X: Cut + if (this.isKeyComboCtrlX(i)) { + if (selection.hasSelection()) { + // Copy selected text into clipboard, then remove the selection + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(s.length()); + scrollToCursor(); + } + return true; + } + + // CTRL+C: Copy + if (this.isKeyComboCtrlC(i)) { + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + } + return true; + } + + // CTRL+V: Paste (insert clipboard contents at caret) + if (this.isKeyComboCtrlV(i)) { + addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); + return true; + } + + // UNDO (Ctrl+Z): restore last entry from undoList and push current state to redoList + if (i == Keyboard.KEY_Z && isCtrlKeyDown()) { + if (undoList.isEmpty()) + return false; // nothing to undo + undoing = true; + redoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = undoList.remove(undoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + return true; + } + + // REDO (Ctrl+Y): opposite of undo + if (i == Keyboard.KEY_Y && isCtrlKeyDown()) { + if (redoList.isEmpty()) + return false; + undoing = true; + undoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = redoList.remove(redoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + return true; + } + + // CTRL+F: format the text according to IndentHelper rules + if (i == Keyboard.KEY_F && isCtrlKeyDown()) { + formatText(); + return true; + } + + // CTRL+/ : toggle comment for selection or current line + if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { + if (selection.hasSelection()) { + toggleCommentSelection(); + } else { + toggleCommentLineAtCursor(); + } + return true; + } + + // CTRL+D : duplicate selection or current line + if (i == Keyboard.KEY_D && isCtrlKeyDown()) { + if (selection.hasSelection()) { + // Multi-line selection duplication: find first and last covered lines, + // then insert the whole block after the last line without adding extra newline. + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + String insertText = selectedText; + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + insertText + text.substring(insertAt)); + // Restore prior selection / cursor positions + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + return true; + } + } else { + // Duplicate current line when nothing is selected + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + int lineStart = line.start, lineEnd = line.end; + String lineText = text.substring(lineStart, lineEnd); + // If the line already ends with a newline, reuse it; otherwise + // prefix a newline so duplicate appears after current line. + String insertText; + if (lineText.endsWith("\n")) { + insertText = lineText; + } else { + insertText = "\n" + lineText; + } + int insertionPoint = lineEnd; + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + return true; + } + } + } + return true; + } + + return false; + } + + /** + * Handles printable character input with auto-pairing for quotes and brackets, + * and smart skipping over existing closing characters. + */ + private boolean handleCharacterInput(char c) { + if (ChatAllowedCharacters.isAllowedCharacter(c)) { + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + // If the user types a closing character and that same closer is + // already immediately after the caret, move caret past it instead + // of inserting another closer. This prevents duplicate closers + // when the editor auto-inserts pairs. + if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { + // Move caret forward by one (skip over existing closer) + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + + // Auto-pair insertion: when opening a quote/brace/bracket is typed, + // insert a matching closer and place caret between the pair. + if (c == '"') { + setText(before + "\"\"" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + if (c == '\'') { + setText(before + "''" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + if (c == '[') { + setText(before + "[]" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + if (c == '(') { + setText(before + "()" + after); + selection.reset(before.length() + 1); + scrollToCursor(); + return true; + } + + // Default insertion for printable characters: insert at caret (replacing selection) + addText(Character.toString(c)); + scrollToCursor(); + return true; + } + return false; + } + + private boolean isShiftKeyDown() { + return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); + } + + // ==================== COMMENT TOGGLING ==================== + // Uses CommentHandler helper for comment operations + + private void toggleCommentSelection() { + CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection( + text, container.lines, selection.getStartSelection(), selection.getEndSelection()); + setText(result.newText); + selection.setStartSelection(result.newStartSelection); + selection.setEndSelection(result.newEndSelection); + } + + private void toggleCommentLineAtCursor() { + CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor( + text, container.lines, selection.getCursorPosition()); + setText(result.newText); + setCursor(result.newCursorPosition, false); + } + + public boolean closeOnEsc(){ + return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + } + + // ==================== KEYBOARD MODIFIERS ==================== + + private boolean isAltKeyDown() { + return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); + } + + private boolean isKeyComboCtrlX(int keyID) { + return keyID == 45 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private boolean isKeyComboCtrlBackspace(int keyID) { + return keyID == Keyboard.KEY_BACK && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private boolean isKeyComboCtrlV(int keyID) { + return keyID == 47 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private boolean isKeyComboCtrlC(int keyID) { + return keyID == 46 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private boolean isKeyComboCtrlA(int keyID) { + return keyID == 30 && isCtrlKeyDown() && !isShiftKeyDown() && !isAltKeyDown(); + } + + private String getIndentCurrentLine() { + for (LineData data : this.container.lines) { + if (selection.getCursorPosition() > data.start && selection.getCursorPosition() <= data.end) { + int i; + for (i = 0; i < data.text.length() && data.text.charAt(i) == ' '; ++i) { + } + return data.text.substring(0, i); + } + } + return ""; + } + + private String getAutoIndentForEnter() { + LineData currentLine = selection.findCurrentLine(container.lines); + + if (currentLine == null) { + return ""; + } + return IndentHelper.getAutoIndentForEnter(currentLine.text, selection.getCursorPosition() - currentLine.start); + } + // ==================== TEXT FORMATTING ==================== + + private int getTabSize() { + return IndentHelper.TAB_SIZE; + } + + private String repeatSpace(int count) { + return IndentHelper.spaces(count); + } + + private void formatText() { + // Calculate viewport width for line wrapping (account for gutter and scrollbar) + int viewportWidth = this.width - LINE_NUMBER_GUTTER_WIDTH - 10; + IndentHelper.FormatResult result = IndentHelper.formatText(text, selection.getCursorPosition(), viewportWidth); + setText(result.text); + selection.reset(Math.max(0, Math.min(result.cursorPosition, this.text.length()))); + } + + // ==================== TAB HANDLING ==================== + + private void handleTab() { + LineData currentLine = selection.findCurrentLine(container.lines); + if (currentLine == null) { + addText(" "); + return; + } + int tab = getTabSize(); + int indentLen = IndentHelper.getLineIndent(currentLine.text); + int textStartPos = currentLine.start + indentLen; + + if (selection.getCursorPosition() <= textStartPos) { + // Cursor before any text: if cursor is exactly at text start, move forward to next tab stop. + // If cursor is inside leading whitespace (before text start), choose nearest tab stop (tie -> forward). + int targetIndent; + if (selection.getCursorPosition() == textStartPos) { + targetIndent = ((indentLen / tab) + 1) * tab; + } else { + int remainder = indentLen % tab; + if (remainder == 0) { + targetIndent = indentLen + tab; // already aligned -> next level + } else { + int down = indentLen - remainder; + int up = indentLen + (tab - remainder); + int distDown = remainder; + int distUp = tab - remainder; + if (distUp < distDown) targetIndent = up; + else if (distUp > distDown) targetIndent = down; + else targetIndent = up; // tie -> forward + } + } + if (targetIndent < 0) targetIndent = 0; + String newIndent = repeatSpace(targetIndent); + String rest = currentLine.text.substring(indentLen); + String before = text.substring(0, currentLine.start); + int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length()); + int sepEnd = Math.min(currentLine.end, text.length()); + String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : ""; + String after = text.substring(sepEnd); + setText(before + newIndent + rest + sep + after); + int newCursor = currentLine.start + targetIndent; + selection.reset(Math.min(newCursor, this.text.length())); + } else { + // Cursor is after start of text: insert spaces at cursor to move following text to next tab stop + int column = selection.getCursorPosition() - currentLine.start; + int targetColumn = ((column / tab) + 1) * tab; + int toInsert = Math.max(0, targetColumn - column); + if (toInsert > 0) { + String spaces = repeatSpace(toInsert); + addText(spaces); + } + } + } + + private void handleShiftTab() { + LineData currentLine = selection.findCurrentLine(container.lines); + if (currentLine == null) + return; + int tab = getTabSize(); + int indentLen = IndentHelper.getLineIndent(currentLine.text); + int textStartPos = currentLine.start + indentLen; + + if (selection.getCursorPosition() <= textStartPos) { + // Cursor before any text: reduce leading indent to previous tab stop + int targetIndent = Math.max(0, ((indentLen - 1) / tab) * tab); + String newIndent = repeatSpace(targetIndent); + String rest = currentLine.text.substring(indentLen); + String before = text.substring(0, currentLine.start); + int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length()); + int sepEnd = Math.min(currentLine.end, text.length()); + String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : ""; + String after = text.substring(sepEnd); + setText(before + newIndent + rest + sep + after); + int newCursor = currentLine.start + targetIndent; + selection.reset(Math.min(newCursor, this.text.length())); + } else { + // Cursor after start of text: remove up to previous tab stop worth of spaces immediately before cursor + int column = selection.getCursorPosition() - currentLine.start; + int mod = column % tab; + int toRemove = mod == 0 ? tab : mod; + int removed = 0; + int pos = selection.getCursorPosition() - 1; + while (pos >= currentLine.start && removed < toRemove && text.charAt(pos) == ' ') { + pos--; + removed++; + } + if (removed > 0) { + int removeStart = pos + 1; + String before = text.substring(0, removeStart); + String after = text.substring(selection.getCursorPosition()); + setText(before + after); + int newCursor = removeStart; + selection.reset(Math.min(newCursor, this.text.length())); + } + } + } + + // ==================== CURSOR MANAGEMENT ==================== + + private void setCursor(int i, boolean select) { + selection.setCursor(i, text != null ? text.length() : 0, select); + } + + private void addText(String s) { + int insertPos = selection.getStartSelection(); + this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText()); + selection.afterTextInsert(insertPos + s.length()); + } + + private int cursorUp() { + return CursorNavigation.cursorUp(selection.getCursorPosition(), container.lines, text); + } + + private int cursorDown() { + return CursorNavigation.cursorDown(selection.getCursorPosition(), container.lines, text); + } + + public String getSelectionBeforeText() { + return selection.getTextBefore(text); + } + + public String getSelectionAfterText() { + return selection.getTextAfter(text); + } + + // ==================== MOUSE HANDLING ==================== + + public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + // Check go to line dialog clicks first + if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + + // Check search bar clicks first + if (searchBar.isVisible() && searchBar.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + + // If search bar is visible but click was outside it, unfocus the search bar + if (searchBar.isVisible()) { + searchBar.unfocus(); + } + + // Let the overlay consume clicks (it returns true when it handled the event) + if (KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton)) + return; + + // Determine whether click occurred inside the text area bounds + this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; + if (this.active) { + // Compute logical click position in text + int clickPos = this.getSelectionPos(xMouse, yMouse); + + // Check if rename refactoring is active and click is in the rename box + if (renameHandler.isActive() && renameHandler.handleClick(clickPos)) { + // Click was handled by rename handler - don't reset selection or do other click handling + this.clicked = false; + activeTextfield = this; + return; + } + + // Normal click handling - reset selection/caret + selection.reset(clickPos); + selection.markActivity(); + + // Prepare click state (left button starts most interactions) + this.clicked = mouseButton == 0; + this.doubleClicked = false; + this.tripleClicked = false; + long time = System.currentTimeMillis(); + + // Prefer delegating scrollbar-start logic to ScrollState. If the click + // is on the scrollbar area and the scrollbar can be dragged, we start + // click-scrolling mode and cancel the normal text click-drag behavior. + if (this.clicked && getPaddedLineCount() * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { + // We consumed the mouse-down as a scrollbar drag start + this.clicked = false; + scroll.startScrollbarDrag(yMouse,this.y,this.height, getPaddedLineCount()); + } else { + // Handle double/triple click selection counting + if (time - this.lastClicked < 300L) { + this.clickCount++; + } else { + this.clickCount = 1; + } + + if (this.clickCount == 2) { + // Double-click: select the word under the caret using the container's word regex + this.doubleClicked = true; + selection.selectWordAtCursor(this.text, this.container.regexWord); + // Prevent subsequent mouse-drag handling in the render loop from + // treating this double-click as a normal click-drag which would + // immediately reset and extend the selection. Clearing `clicked` + // keeps the double-click selection stable. + this.clicked = false; + } else if (this.clickCount >= 3) { + // Triple-click: select the entire logical line that contains the caret + this.tripleClicked = true; + selection.selectLineAtCursor(container.lines); + // Same as double-click: clear `clicked` to avoid accidental drag-extension + this.clicked = false; + this.clickCount = 0; + } + } + + this.lastClicked = time; + activeTextfield = this; + } + } + + // Called from GuiScreen.updateScreen() + public void updateCursorCounter() { + // Only process KeyPresets if search bar and go-to-line dialog don't have focus + // This prevents COPY, PASTE, UNDO, etc. from firing when typing in dialogs + KEYS.tick(); + + searchBar.updateCursor(); + goToLineDialog.updateCursor(); + renameHandler.updateCursor(); + ++this.cursorCounter; + } + + // ==================== TEXT MANAGEMENT ==================== + + public void setText(String text) { + if (text == null) { + return; + } + + text = text.replace("\r", ""); + text = text.replace("\t", " "); + // preserve trailing newlines here — JavaTextContainer.init will + // ignore an extra empty final split when rendering lines. + if (this.text == null || !this.text.equals(text)) { + if (this.listener != null) { + this.listener.textUpdate(text); + } + + if (!this.undoing) { + this.undoList.add(new GuiScriptTextArea1.UndoData(this.text, selection.getCursorPosition())); + this.redoList.clear(); + } + + this.text = text; + //this.container = new TextContainer(text); + if (this.container == null) + this.container = new ScriptTextContainer(text); + + this.container.init(text, this.width, this.height); + + if (this.enableCodeHighlighting) + this.container.formatCodeText(); + + // Ensure scroll state stays in bounds after text change + int maxScroll = Math.max(0, getPaddedLineCount() - this.container.visibleLines); + scroll.clampToBounds(maxScroll); + + selection.clamp(this.text.length()); + + // Consider text changes user activity to pause caret blinking briefly + selection.markActivity(); + searchBar.updateMatches(); + + } + } + + public String getText() { + return this.text; + } + + public boolean isEnabled() { + return this.enabled && this.visible; + } + + public boolean hasVerticalScrollbar() { + return this.container != null && this.container.visibleLines < getPaddedLineCount(); + } + + public void enableCodeHighlighting() { + this.enableCodeHighlighting = true; + this.container.formatCodeText(); + } + + public void setListener(ITextChangeListener listener) { + this.listener = listener; + } + + private void clampSelectionBounds() { + selection.clamp(text != null ? text.length() : 0); + } + + // ==================== INNER CLASSES ==================== + + public static class UndoData { + public String text; + public int cursorPosition; + + public UndoData(String text, int cursorPosition) { + this.text = text; + this.cursorPosition = cursorPosition; + } + } +} 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 e3808a35f..16e604d19 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 @@ -121,7 +121,7 @@ public void formatCodeText() { /** * Convert new interpreter TokenType to legacy JavaTextContainer.TokenType format. */ - private JavaTextContainer.TokenType toLegacyTokenType(TokenType type) { + private JavaTextContainer.TokenType toLegacyTokenType(noppes.npcs.client.gui.util.script.interpreter.TokenType type) { if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.COMMENT) return JavaTextContainer.TokenType.COMMENT; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.STRING) return JavaTextContainer.TokenType.STRING; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_KEYWORD) return JavaTextContainer.TokenType.CLASS_KEYWORD; From 555fb481d38439746ac2622cf0e324ab7aaae6a4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 23 Dec 2025 14:40:50 +0200 Subject: [PATCH 003/337] More fixes and improvements --- .../util/script/interpreter/FieldInfo.java | 17 + .../util/script/interpreter/MethodInfo.java | 18 + .../script/interpreter/ScriptDocument.java | 355 +++++++++++++++--- .../util/script/interpreter/ScriptLine.java | 32 +- .../gui/util/script/interpreter/TypeInfo.java | 38 ++ .../util/script/interpreter/TypeResolver.java | 7 + 6 files changed, 420 insertions(+), 47 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java index 8d4e85e09..dc8f0da89 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -48,10 +48,27 @@ public static FieldInfo unresolved(String name, Scope scope) { return new FieldInfo(name, scope, null, -1, false, null); } + /** + * Create a FieldInfo from reflection data for method parameters. + */ + public static FieldInfo reflectionParam(String name, TypeInfo type) { + return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null); + } + + /** + * Create a FieldInfo from reflection data for a class field. + */ + public static FieldInfo fromReflection(java.lang.reflect.Field field, TypeInfo containingType) { + String name = field.getName(); + TypeInfo type = TypeInfo.fromClass(field.getType()); + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null); + } + // Getters public String getName() { return name; } public Scope getScope() { return scope; } public TypeInfo getDeclaredType() { return declaredType; } + public TypeInfo getTypeInfo() { return declaredType; } // Alias for getDeclaredType public int getDeclarationOffset() { return declarationOffset; } public boolean isResolved() { return resolved; } public MethodInfo getContainingMethod() { return containingMethod; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 95b9b76a5..58f229797 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -58,6 +58,24 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { return new MethodInfo(name, null, null, params, -1, -1, -1, false, false); } + /** + * Create a MethodInfo from reflection data. + * Used when resolving method calls on known types. + */ + public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInfo containingType) { + String name = method.getName(); + TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); + + List params = new ArrayList<>(); + Class[] paramTypes = method.getParameterTypes(); + for (int i = 0; i < paramTypes.length; i++) { + TypeInfo paramType = TypeInfo.fromClass(paramTypes[i]); + params.add(FieldInfo.reflectionParam("arg" + i, paramType)); + } + + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false); + } + // Getters public String getName() { return name; } public TypeInfo getReturnType() { return returnType; } 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 33399c954..b30ac20d5 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 @@ -40,9 +40,9 @@ public class ScriptDocument { private static final Pattern KEYWORD_PATTERN = Pattern.compile( "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); - // Declarations + // Declarations - Updated to capture method parameters private static final Pattern IMPORT_PATTERN = Pattern.compile( - "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*?)?\\s*(?:;|$)"); private static final Pattern CLASS_DECL_PATTERN = Pattern.compile( "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); private static final Pattern METHOD_DECL_PATTERN = Pattern.compile( @@ -66,6 +66,9 @@ public class ScriptDocument { private final Map globalFields = new HashMap<>(); private final Set wildcardPackages = new HashSet<>(); private Map importsBySimpleName = new HashMap<>(); + + // Local variables per method (methodStartOffset -> {varName -> FieldInfo}) + private final Map> methodLocals = new HashMap<>(); // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); @@ -171,6 +174,7 @@ public void formatCodeText() { globalFields.clear(); wildcardPackages.clear(); excludedRanges.clear(); + methodLocals.clear(); // Phase 1: Find excluded regions (strings/comments) findExcludedRanges(); @@ -178,7 +182,7 @@ public void formatCodeText() { // Phase 2: Parse imports parseImports(); - // Phase 3: Parse structure (methods, fields) + // Phase 3: Parse structure (methods, fields, locals) parseStructure(); // Phase 4: Build marks and assign to lines @@ -189,7 +193,7 @@ public void formatCodeText() { // Phase 6: Build tokens for each line for (ScriptLine line : lines) { - line.buildTokensFromMarks(marks, text); + line.buildTokensFromMarks(marks, text, this); } // Phase 7: Compute indent guides @@ -259,6 +263,11 @@ private void parseImports() { String matchText = m.group(0); boolean isWildcard = matchText.contains("*"); boolean isStatic = matchText.contains("static"); + + // Skip if path ends with dot (incomplete import) + if (fullPath.endsWith(".")) { + // continue; + } int lastDot = fullPath.lastIndexOf('.'); String simpleName = isWildcard ? null : (lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath); @@ -284,6 +293,9 @@ private void parseStructure() { // Parse methods parseMethodDeclarations(); + // Parse local variables inside methods + parseLocalVariables(); + // Parse global fields (outside methods) parseGlobalFields(); } @@ -306,8 +318,8 @@ private void parseMethodDeclarations() { if (bodyEnd < 0) bodyEnd = text.length(); - // Parse parameters - List params = parseParameters(paramList, m.start()); + // Parse parameters with their actual positions + List params = parseParametersWithPositions(paramList, m.start(3)); MethodInfo methodInfo = MethodInfo.declaration( methodName, @@ -340,12 +352,13 @@ private void parseMethodDeclarations() { } } - private List parseParameters(String paramList, int declOffset) { + private List parseParametersWithPositions(String paramList, int paramListStart) { List params = new ArrayList<>(); if (paramList == null || paramList.trim().isEmpty()) { return params; } + // Pattern: Type varName (with optional spaces) Pattern paramPattern = Pattern.compile( "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); Matcher m = paramPattern.matcher(paramList); @@ -353,11 +366,50 @@ private List parseParameters(String paramList, int declOffset) { String typeName = m.group(1); String paramName = m.group(2); TypeInfo typeInfo = resolveType(typeName); - params.add(FieldInfo.parameter(paramName, typeInfo, declOffset, null)); + // Store the absolute position of the parameter name + int paramNameStart = paramListStart + m.start(2); + params.add(FieldInfo.parameter(paramName, typeInfo, paramNameStart, null)); } return params; } + private void parseLocalVariables() { + for (MethodInfo method : methods) { + Map locals = new HashMap<>(); + methodLocals.put(method.getDeclarationOffset(), locals); + + int bodyStart = method.getBodyStart(); + int bodyEnd = method.getBodyEnd(); + if (bodyStart < 0 || bodyEnd <= bodyStart) continue; + + String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length())); + + // Pattern for local variable declarations: Type varName = or Type varName; + Pattern localDecl = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;|,)"); + Matcher m = localDecl.matcher(bodyText); + while (m.find()) { + int absPos = bodyStart + m.start(); + if (isExcluded(absPos)) continue; + + String typeName = m.group(1); + String varName = m.group(2); + + // Skip if it looks like a method call or control flow + if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || + typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || + typeName.equals("new") || typeName.equals("throw")) { + continue; + } + + TypeInfo typeInfo = resolveType(typeName); + int declPos = bodyStart + m.start(2); + FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method); + locals.put(varName, fieldInfo); + } + } + } + private void parseGlobalFields() { Matcher m = FIELD_DECL_PATTERN.matcher(text); while (m.find()) { @@ -453,22 +505,125 @@ private void addPatternMarks(List marks, Pattern pattern, Token private void markImports(List marks) { for (ImportData imp : imports) { // Mark 'import' keyword - marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.IMPORT_KEYWORD)); + marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.IMPORT_KEYWORD, imp)); - // Mark the path + // Parse path tokens int pathStart = imp.getPathStartOffset(); int pathEnd = imp.getPathEndOffset(); - - if (imp.isResolved()) { - // Resolved import - mark as type declaration - if (imp.getResolvedType() != null) { - marks.add(new ScriptLine.Mark(pathStart, pathEnd, imp.getResolvedType().getTokenType())); + String pathText = text.substring(pathStart, Math.min(pathEnd, text.length())); + + // Skip if path ends with dot (incomplete) + if (pathText.trim().endsWith(".")) { + continue; + } + + // Tokenize the path + List tokens = new ArrayList<>(); + List tokenStarts = new ArrayList<>(); + List tokenEnds = new ArrayList<>(); + Pattern idPattern = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*"); + Matcher idm = idPattern.matcher(pathText); + while (idm.find()) { + tokens.add(idm.group()); + tokenStarts.add(pathStart + idm.start()); + tokenEnds.add(pathStart + idm.end()); + } + + if (tokens.isEmpty()) continue; + + if (imp.isWildcard()) { + // Wildcard import: mark all tokens as package (blue) if valid + boolean pkgValid = typeResolver.isValidPackage(imp.getFullPath()); + + // Also check if it might be a class wildcard (OuterClass.*) + TypeInfo outerType = typeResolver.resolveFullName(imp.getFullPath()); + + if (pkgValid || outerType != null) { + // Mark package portion in blue + int pkgTokenCount = outerType != null ? tokens.size() - 1 : tokens.size(); + for (int i = 0; i < Math.min(pkgTokenCount, tokens.size()); i++) { + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.TYPE_DECL, imp)); + } + // If it's a class wildcard, mark the class with its type + if (outerType != null && tokens.size() > 0) { + int lastIdx = tokens.size() - 1; + marks.add(new ScriptLine.Mark(tokenStarts.get(lastIdx), tokenEnds.get(lastIdx), + outerType.getTokenType(), imp)); + } } else { - marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + // Unknown package/class - mark as undefined + marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR, imp)); } } else { - // Unresolved import - marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); + // Non-wildcard import: package is blue, class name(s) get type colors + TypeInfo resolvedType = imp.getResolvedType(); + + if (resolvedType != null && resolvedType.isResolved()) { + // Count package segments + String fullPath = imp.getFullPath(); + String resolvedName = resolvedType.getFullName(); + String pkgName = resolvedType.getPackageName(); + + int pkgSegments = pkgName != null && !pkgName.isEmpty() + ? pkgName.split("\\.").length : 0; + + // Mark package tokens in blue + for (int i = 0; i < Math.min(pkgSegments, tokens.size()); i++) { + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.TYPE_DECL, imp)); + } + + // Mark class tokens with appropriate type colors + // For inner classes, each segment might have a different type + for (int i = pkgSegments; i < tokens.size(); i++) { + String segmentName = tokens.get(i); + // Try to resolve this specific class/inner class + StringBuilder classPath = new StringBuilder(); + if (pkgName != null && !pkgName.isEmpty()) { + classPath.append(pkgName).append("."); + } + for (int j = pkgSegments; j <= i; j++) { + if (j > pkgSegments) classPath.append("$"); + classPath.append(tokens.get(j)); + } + + TypeInfo segmentType = typeResolver.resolveFullName(classPath.toString()); + TokenType tokenType = segmentType != null + ? segmentType.getTokenType() + : resolvedType.getTokenType(); + + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), tokenType, imp)); + } + } else { + // Try to figure out which part is invalid + // Mark valid package portion in blue, invalid portion in red + String fullPath = imp.getFullPath(); + int lastValidPkg = -1; + StringBuilder pkgBuilder = new StringBuilder(); + + for (int i = 0; i < tokens.size(); i++) { + if (i > 0) pkgBuilder.append("."); + pkgBuilder.append(tokens.get(i)); + + // Check if this is a valid package + if (typeResolver.isValidPackage(pkgBuilder.toString())) { + lastValidPkg = i; + } + } + + if (lastValidPkg >= 0) { + // Mark valid package in blue + for (int i = 0; i <= lastValidPkg; i++) { + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.TYPE_DECL, imp)); + } + // Mark rest as undefined + for (int i = lastValidPkg + 1; i < tokens.size(); i++) { + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.UNDEFINED_VAR, imp)); + } + } else { + // Everything is unresolved + marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR, imp)); + } + } } } } @@ -532,6 +687,7 @@ private void markMethodCalls(List marks) { while (m.find()) { int start = m.start(1); int end = m.end(1); + String methodName = m.group(1); if (isExcluded(start)) continue; @@ -558,21 +714,42 @@ private void markMethodCalls(List marks) { } } - // Determine if we should highlight this as a method call - boolean shouldHighlight = true; - if (hasReceiver && receiverName != null && !receiverName.isEmpty()) { - // Check if receiver is an unresolved class reference + // Check if receiver is an uppercase class reference (static call) if (Character.isUpperCase(receiverName.charAt(0))) { TypeInfo recvType = resolveType(receiverName); if (recvType == null || !recvType.isResolved()) { - // Don't highlight method calls on unresolved types - shouldHighlight = false; + // Method call on unresolved type - mark as undefined + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + continue; + } + // Check if method exists on resolved type + if (!recvType.hasMethod(methodName) ) { + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + continue; + } + // Valid method call on static type + MethodInfo methodInfo = recvType.getMethodInfo(methodName); + marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo)); + } else { + // Lower-case receiver - this is a method call on an instance + // Try to resolve the receiver's type + FieldInfo receiverField = resolveVariable(receiverName, start); + if (receiverField != null && receiverField.getTypeInfo() != null) { + TypeInfo recvType = receiverField.getTypeInfo(); + if (recvType.hasMethod(methodName)) { + MethodInfo methodInfo = recvType.getMethodInfo(methodName); + marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo)); + } else { + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + } + } else { + // Can't resolve receiver - mark method call anyway (could be valid at runtime) + marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); } } - } - - if (shouldHighlight) { + } else { + // No receiver - standalone method call (local function or global) marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); } } @@ -589,6 +766,15 @@ private void markVariables(List marks) { "throws", "super", "assert", "volatile", "transient" )); + // First pass: mark method parameters in their declaration positions + for (MethodInfo method : methods) { + for (FieldInfo param : method.getParameters()) { + int pos = param.getDeclarationOffset(); + String name = param.getName(); + marks.add(new ScriptLine.Mark(pos, pos + name.length(), TokenType.PARAMETER, param)); + } + } + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); @@ -601,7 +787,7 @@ private void markVariables(List marks) { if (knownKeywords.contains(name)) continue; - // Skip if preceded by dot (field access) + // Skip if preceded by dot (field access - handled separately) if (isPrecededByDot(position)) continue; @@ -613,29 +799,53 @@ private void markVariables(List marks) { if (Character.isUpperCase(name.charAt(0))) continue; + // Skip method calls (followed by paren) + if (isFollowedByParen(m.end(1))) + continue; + // Find containing method MethodInfo containingMethod = findMethodAtPosition(position); if (containingMethod != null) { // Check parameters if (containingMethod.hasParameter(name)) { - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER)); + FieldInfo paramInfo = containingMethod.getParameter(name); + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER, paramInfo)); continue; } - } - // Check global fields - if (globalFields.containsKey(name)) { - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); - continue; - } + // Check local variables + Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); + if (locals != null && locals.containsKey(name)) { + FieldInfo localInfo = locals.get(name); + // Only highlight if the position is after the declaration + if (localInfo.isVisibleAt(position)) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, localInfo)); + continue; + } + } - // Check if it's a method call (followed by paren) - if (isFollowedByParen(m.end(1))) - continue; + // Check global fields + if (globalFields.containsKey(name)) { + FieldInfo fieldInfo = globalFields.get(name); + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, fieldInfo)); + continue; + } + + // Unknown variable inside method - mark as undefined + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + } else { + // Outside any method + // Check global fields + if (globalFields.containsKey(name)) { + FieldInfo fieldInfo = globalFields.get(name); + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, fieldInfo)); + continue; + } - // Unknown identifier - could mark as undefined, but be conservative - // Only mark as undefined if it looks like a variable usage + // Unknown variable outside method - mark as undefined + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + } } } @@ -652,13 +862,42 @@ private void markImportedClassUsages(List marks) { if (isExcluded(start)) continue; + // Skip in import/package statements + if (isInImportOrPackage(start)) + continue; + // Try to resolve the class TypeInfo info = resolveType(className); if (info != null && info.isResolved()) { - marks.add(new ScriptLine.Mark(start, end, info.getTokenType())); - } else if (!importsBySimpleName.containsKey(className) && - !TypeResolver.JAVA_LANG_CLASSES.contains(className) && - wildcardPackages.isEmpty()) { + marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), info)); + } else { + // Unknown type - mark as undefined + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + } + } + + // Also mark uppercase identifiers in type positions (new X(), X variable, etc.) + Pattern typeUsage = Pattern.compile("\\b(new\\s+)?([A-Z][a-zA-Z0-9_]*)(?:\\s*<[^>]*>)?\\s*(?:\\(|\\[|\\b[a-z])"); + Matcher tm = typeUsage.matcher(text); + + while (tm.find()) { + String className = tm.group(2); + int start = tm.start(2); + int end = tm.end(2); + + if (isExcluded(start)) + continue; + + // Skip in import/package statements + if (isInImportOrPackage(start)) + continue; + + // Try to resolve the class + TypeInfo info = resolveType(className); + if (info != null && info.isResolved()) { + marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), info)); + } else { + // Unknown type - mark as undefined marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); } } @@ -771,6 +1010,34 @@ class OpenBrace { // ==================== UTILITY METHODS ==================== + private FieldInfo resolveVariable(String name, int position) { + // Find containing method + MethodInfo containingMethod = findMethodAtPosition(position); + + if (containingMethod != null) { + // Check parameters + if (containingMethod.hasParameter(name)) { + return containingMethod.getParameter(name); + } + + // Check local variables + Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); + if (locals != null && locals.containsKey(name)) { + FieldInfo localInfo = locals.get(name); + if (localInfo.isVisibleAt(position)) { + return localInfo; + } + } + } + + // Check global fields + if (globalFields.containsKey(name)) { + return globalFields.get(name); + } + + return null; + } + private boolean isPrecededByDot(int position) { if (position <= 0) return false; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 019c4d7aa..819c27965 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -102,11 +102,13 @@ public void addToken(Token token) { /** * Build tokens from a list of marks (highlight regions). * Fills gaps between marks with DEFAULT tokens. + * Attaches metadata from marks to the created tokens. * * @param marks List of highlight marks that overlap this line * @param fullText The complete document text + * @param document The parent ScriptDocument (for context lookups) */ - public void buildTokensFromMarks(List marks, String fullText) { + public void buildTokensFromMarks(List marks, String fullText, ScriptDocument document) { clearTokens(); int cursor = globalStart; @@ -131,10 +133,24 @@ public void buildTokensFromMarks(List marks, String fullText) { addToken(Token.defaultToken(gapText, cursor, gapEnd)); } - // Add the marked token + // Add the marked token with metadata if (tokenStart < tokenEnd) { String tokenText = fullText.substring(tokenStart, tokenEnd); Token token = new Token(tokenText, tokenStart, tokenEnd, mark.type); + + // Attach metadata based on type + if (mark.metadata != null) { + if (mark.metadata instanceof TypeInfo) { + token.setTypeInfo((TypeInfo) mark.metadata); + } else if (mark.metadata instanceof FieldInfo) { + token.setFieldInfo((FieldInfo) mark.metadata); + } else if (mark.metadata instanceof MethodInfo) { + token.setMethodInfo((MethodInfo) mark.metadata); + } else if (mark.metadata instanceof ImportData) { + token.setImportData((ImportData) mark.metadata); + } + } + addToken(token); } @@ -283,21 +299,31 @@ public String toString() { /** * A simple mark representing a highlighted region. * Used during token building phase. + * Can optionally carry metadata (TypeInfo, FieldInfo, MethodInfo, ImportData). */ public static class Mark { public final int start; public final int end; public final TokenType type; + public final Object metadata; public Mark(int start, int end, TokenType type) { this.start = start; this.end = end; this.type = type; + this.metadata = null; + } + + public Mark(int start, int end, TokenType type, Object metadata) { + this.start = start; + this.end = end; + this.type = type; + this.metadata = metadata; } @Override public String toString() { - return "Mark{" + type + " [" + start + "-" + end + "]}"; + return "Mark{" + type + " [" + start + "-" + end + "]" + (metadata != null ? " " + metadata.getClass().getSimpleName() : "") + "}"; } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java index 544bdadf3..e2b0099af 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -153,6 +153,44 @@ public boolean hasField(String fieldName) { return false; } + /** + * Get MethodInfo for a method by name. Returns null if not found. + * Creates a synthetic MethodInfo based on reflection data. + */ + public MethodInfo getMethodInfo(String methodName) { + if (javaClass == null) return null; + try { + for (java.lang.reflect.Method m : javaClass.getMethods()) { + if (m.getName().equals(methodName)) { + // Create a synthetic MethodInfo from reflection + return MethodInfo.fromReflection(m, this); + } + } + } catch (Exception e) { + // Security or linkage error + } + return null; + } + + /** + * Get FieldInfo for a field by name. Returns null if not found. + * Creates a synthetic FieldInfo based on reflection data. + */ + public FieldInfo getFieldInfo(String fieldName) { + if (javaClass == null) return null; + try { + for (java.lang.reflect.Field f : javaClass.getFields()) { + if (f.getName().equals(fieldName)) { + // Create a synthetic FieldInfo from reflection + return FieldInfo.fromReflection(f, this); + } + } + } catch (Exception e) { + // Security or linkage error + } + return null; + } + @Override public String toString() { return "TypeInfo{" + fullName + ", " + kind + ", resolved=" + resolved + "}"; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java index 0c8794c85..a962ec9e2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter; +import noppes.npcs.client.gui.util.script.PackageFinder; + import java.util.*; /** @@ -213,6 +215,11 @@ public boolean isValidPackage(String packagePath) { return true; } + if (PackageFinder.find(packagePath)) { + registerPackage(packagePath); + return true; + } + // Try to find a class in this package to validate it String[] testClasses = getTestClassesForPackage(packagePath); for (String testClass : testClasses) { From 74fe7cb892106f0f518eef255ce9c16f89a58f94 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 23 Dec 2025 17:12:03 +0200 Subject: [PATCH 004/337] Fixed generics List and their depths List not working properly. Added chained fields highlight like mc.thePlayer.worldObj Decreased UNDEFINED_VAR priority. --- .../script/interpreter/ScriptDocument.java | 361 +++++++++++++++++- .../util/script/interpreter/TokenType.java | 2 +- 2 files changed, 341 insertions(+), 22 deletions(-) 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 b30ac20d5..f46a7fb13 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 @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter; +import net.minecraft.client.Minecraft; import noppes.npcs.client.ClientProxy; import java.util.*; @@ -477,7 +478,6 @@ private List buildMarks() { // Methods markMethodDeclarations(marks); - markMethodCalls(marks); // Numbers addPatternMarks(marks, NUMBER_PATTERN, TokenType.NUMBER); @@ -485,6 +485,11 @@ private List buildMarks() { // Variables and fields markVariables(marks); + // Chained field accesses (e.g., mc.player.world, this.field) + markChainedFieldAccesses(marks); + + markMethodCalls(marks); + // Imported class usages markImportedClassUsages(marks); @@ -594,9 +599,8 @@ private void markImports(List marks) { marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), tokenType, imp)); } } else { - // Try to figure out which part is invalid - // Mark valid package portion in blue, invalid portion in red - String fullPath = imp.getFullPath(); + // Try to figure out which parts are valid vs invalid + // First, try to find valid package segments int lastValidPkg = -1; StringBuilder pkgBuilder = new StringBuilder(); @@ -609,18 +613,67 @@ private void markImports(List marks) { lastValidPkg = i; } } + + // Now try to resolve outer classes that might exist after the package + // For import like kamkeel.api.IOverlay.idk where IOverlay exists but idk doesn't + int lastValidClass = -1; + TypeInfo lastValidType = null; + StringBuilder classPath = new StringBuilder(); if (lastValidPkg >= 0) { - // Mark valid package in blue - for (int i = 0; i <= lastValidPkg; i++) { - marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.TYPE_DECL, imp)); + classPath.append(pkgBuilder.substring(0, + pkgBuilder.toString().indexOf(tokens.get(lastValidPkg)) + tokens.get(lastValidPkg) + .length())); + } + + for (int i = lastValidPkg + 1; i < tokens.size(); i++) { + if (classPath.length() > 0) + classPath.append(i == lastValidPkg + 1 ? "." : "$"); + classPath.append(tokens.get(i)); + + TypeInfo segmentType = typeResolver.resolveFullName(classPath.toString()); + if (segmentType != null && segmentType.isResolved()) { + lastValidClass = i; + lastValidType = segmentType; + } else { + break; // Stop at first unresolved segment } - // Mark rest as undefined - for (int i = lastValidPkg + 1; i < tokens.size(); i++) { - marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.UNDEFINED_VAR, imp)); + } + + // Mark valid package portion in blue + for (int i = 0; i <= lastValidPkg; i++) { + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.TYPE_DECL, imp)); + } + + // Mark valid class segments with their type colors + StringBuilder resolvedPath = new StringBuilder(); + if (lastValidPkg >= 0) { + for (int i = 0; i <= lastValidPkg; i++) { + if (i > 0) + resolvedPath.append("."); + resolvedPath.append(tokens.get(i)); } - } else { - // Everything is unresolved + } + + for (int i = lastValidPkg + 1; i <= lastValidClass; i++) { + if (resolvedPath.length() > 0) + resolvedPath.append(i == lastValidPkg + 1 ? "." : "$"); + resolvedPath.append(tokens.get(i)); + + TypeInfo segmentType = typeResolver.resolveFullName(resolvedPath.toString()); + TokenType tokenType = (segmentType != null) ? segmentType.getTokenType() : TokenType.IMPORTED_CLASS; + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), tokenType, imp)); + } + + // Mark remaining unresolved segments as undefined (red) + int firstUnresolved = Math.max(lastValidPkg + 1, lastValidClass + 1); + for (int i = firstUnresolved; i < tokens.size(); i++) { + marks.add(new ScriptLine.Mark(tokenStarts.get(i), tokenEnds.get(i), TokenType.UNDEFINED_VAR, + imp)); + } + + // If nothing was valid at all, mark everything as undefined + if (lastValidPkg < 0 && lastValidClass < 0) { marks.add(new ScriptLine.Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR, imp)); } } @@ -652,20 +705,151 @@ private void markClassDeclarations(List marks) { } private void markTypeDeclarations(List marks) { - // Pattern for type followed by variable name - Pattern typeDecl = Pattern.compile( + // Pattern for type optionally followed by generics - we'll manually parse generics + Pattern typeStart = Pattern.compile( "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + - "([A-Z][a-zA-Z0-9_]*)\\s*(?:<[^>]+>)?\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + "([A-Z][a-zA-Z0-9_]*)\\s*"); - Matcher m = typeDecl.matcher(text); - while (m.find()) { - if (isExcluded(m.start())) + Matcher m = typeStart.matcher(text); + int searchFrom = 0; + + while (m.find(searchFrom)) { + int typeNameStart = m.start(1); + int typeNameEnd = m.end(1); + + if (isExcluded(typeNameStart)) { + searchFrom = m.end(); continue; + } String typeName = m.group(1); - TypeInfo info = resolveType(typeName); - TokenType tokenType = (info != null && info.isResolved()) ? info.getTokenType() : TokenType.UNDEFINED_VAR; - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), tokenType)); + int posAfterType = m.end(1); + + // Skip whitespace after type name + while (posAfterType < text.length() && Character.isWhitespace(text.charAt(posAfterType))) { + posAfterType++; + } + + // Check for generic parameters + String genericContent = null; + int genericStart = -1; + int genericEnd = -1; + + if (posAfterType < text.length() && text.charAt(posAfterType) == '<') { + genericStart = posAfterType; + int depth = 1; + int i = posAfterType + 1; + while (i < text.length() && depth > 0) { + char c = text.charAt(i); + if (c == '<') + depth++; + else if (c == '>') + depth--; + i++; + } + if (depth == 0) { + genericEnd = i; + genericContent = text.substring(genericStart + 1, genericEnd - 1); + posAfterType = genericEnd; + } + } + + // Skip whitespace after generics + while (posAfterType < text.length() && Character.isWhitespace(text.charAt(posAfterType))) { + posAfterType++; + } + + // Check if this looks like a type declaration: + boolean hasGeneric = genericContent != null && !genericContent.isEmpty(); + boolean followedByVarName = false; + boolean atEndOfLine = posAfterType >= text.length() || text.charAt(posAfterType) == '\n'; + + if (!atEndOfLine && posAfterType < text.length()) { + char nextChar = text.charAt(posAfterType); + followedByVarName = Character.isLetter(nextChar) || nextChar == '_'; + } + + // Accept as type if: has generics OR followed by variable name + if (hasGeneric || followedByVarName) { + // Resolve the main type + TypeInfo info = resolveType(typeName); + TokenType tokenType = (info != null && info.isResolved()) ? info.getTokenType() : TokenType.UNDEFINED_VAR; + marks.add(new ScriptLine.Mark(typeNameStart, typeNameEnd, tokenType, info)); + + // Handle generic content recursively + if (hasGeneric && genericStart >= 0) { + int contentStart = genericStart + 1; + markGenericTypesRecursive(genericContent, contentStart, marks); + } + } + + searchFrom = m.end(); + } + } + + /** + * Recursively parse and mark generic type parameters. + * Handles arbitrarily nested generics like Map>>. + */ + private void markGenericTypesRecursive(String content, int baseOffset, List marks) { + if (content == null || content.isEmpty()) + return; + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // Skip whitespace and punctuation + if (!Character.isJavaIdentifierStart(c)) { + i++; + continue; + } + + // Found start of identifier + int start = i; + while (i < content.length() && Character.isJavaIdentifierPart(content.charAt(i))) { + i++; + } + String typeName = content.substring(start, i); + + // Only process if it looks like a type name (starts with uppercase) + if (Character.isUpperCase(typeName.charAt(0))) { + int absStart = baseOffset + start; + int absEnd = baseOffset + i; + + if (!isExcluded(absStart)) { + TypeInfo info = resolveType(typeName); + TokenType tokenType = (info != null && info.isResolved()) ? info.getTokenType() : TokenType.UNDEFINED_VAR; + marks.add(new ScriptLine.Mark(absStart, absEnd, tokenType, info)); + } + } + + // Skip whitespace + while (i < content.length() && Character.isWhitespace(content.charAt(i))) { + i++; + } + + // Check for nested generic + if (i < content.length() && content.charAt(i) == '<') { + int nestedStart = i + 1; + int depth = 1; + i++; + + // Find matching > + while (i < content.length() && depth > 0) { + if (content.charAt(i) == '<') + depth++; + else if (content.charAt(i) == '>') + depth--; + i++; + } + + // Recursively parse nested content + if (nestedStart < i - 1) { + String nestedContent = content.substring(nestedStart, i - 1); + markGenericTypesRecursive(nestedContent, baseOffset + nestedStart, marks); + } + } } } @@ -734,6 +918,8 @@ private void markMethodCalls(List marks) { } else { // Lower-case receiver - this is a method call on an instance // Try to resolve the receiver's type + + //TODO IMPROVE HERE: currently only resolves global fields, not chained fields or 'this' FieldInfo receiverField = resolveVariable(receiverName, start); if (receiverField != null && receiverField.getTypeInfo() != null) { TypeInfo recvType = receiverField.getTypeInfo(); @@ -744,6 +930,7 @@ private void markMethodCalls(List marks) { marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); } } else { + // TODO weatherEffects.gest() is marked valid here, despite List having no gest() method // Can't resolve receiver - mark method call anyway (could be valid at runtime) marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); } @@ -849,6 +1036,138 @@ private void markVariables(List marks) { } } + /** + * Mark chained field accesses like: mc.player.world, array.length, this.field, etc. + * This handles dot-separated access chains and colors each segment appropriately. + */ + private void markChainedFieldAccesses(List marks) { + // Pattern to find identifier chains: identifier.identifier, this.identifier, etc. + // Start with an identifier or 'this' followed by at least one dot and another identifier + Pattern chainPattern = Pattern.compile("\\b(this|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher m = chainPattern.matcher(text); + + while (m.find()) { + int chainStart = m.start(1); + + if (isExcluded(chainStart)) + continue; + + // Skip import/package statements + if (isInImportOrPackage(chainStart)) + continue; + + // Build the full chain by continuing to match subsequent .identifier patterns + List chainSegments = new ArrayList<>(); + List segmentPositions = new ArrayList<>(); // [start, end] for each segment + + String firstSegment = m.group(1); + chainSegments.add(firstSegment); + segmentPositions.add(new int[]{m.start(1), m.end(1)}); + + String secondSegment = m.group(2); + chainSegments.add(secondSegment); + segmentPositions.add(new int[]{m.start(2), m.end(2)}); + + // Continue reading more segments + int pos = m.end(2); + while (pos < text.length()) { + // Skip whitespace + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + + if (pos >= text.length() || text.charAt(pos) != '.') + break; + pos++; // Skip the dot + + // Skip whitespace after dot + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + + if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) + break; + + // Read next identifier + int identStart = pos; + while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) + pos++; + int identEnd = pos; + + // Check if this is followed by parentheses (method call - stop) + int checkPos = pos; + while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos))) + checkPos++; + if (checkPos < text.length() && text.charAt(checkPos) == '(') { + break; // This is a method call, not a field + } + + chainSegments.add(text.substring(identStart, identEnd)); + segmentPositions.add(new int[]{identStart, identEnd}); + } + + // Now resolve the chain type by type + // Start with resolving the first segment + TypeInfo currentType = null; + boolean firstIsThis = firstSegment.equals("this"); + + if (firstIsThis) { + // For 'this', we don't have a class context to resolve, but we can mark subsequent fields + // as global fields if they exist in globalFields + currentType = null; // We'll use globalFields for the next segment + } else if (Character.isUpperCase(firstSegment.charAt(0))) { + // Static field access like SomeClass.field + currentType = resolveType(firstSegment); + } else { + // Variable field access + FieldInfo varInfo = resolveVariable(firstSegment, chainStart); + if (varInfo != null) { + currentType = varInfo.getTypeInfo(); + } + } + + // Mark the segments - skip first segment (already marked by markVariables or markImportedClassUsages) + for (int i = 1; i < chainSegments.size(); i++) { + String segment = chainSegments.get(i); + int[] segPos = segmentPositions.get(i); + + if (isExcluded(segPos[0])) + continue; + + if (i == 1 && firstIsThis) { + // For "this.fiel/d", check if field exists in globalFields + if (globalFields.containsKey(segment)) { + FieldInfo fieldInfo = globalFields.get(segment); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + currentType = fieldInfo.getTypeInfo(); + } else { + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); + currentType = null; + } + } else if (currentType != null && currentType.isResolved()) { + // Check if this type has this field + //TODO: THIS MAY NEED TO HANDLE STATIC VS INSTANCE FIELDS WITH DIFFERENT RENDERING + if (currentType.hasField(segment)) { + FieldInfo fieldInfo = currentType.getFieldInfo(segment); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + // Update currentType for next segment + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + + /*TODO THIS ELSE OVERRIDES FOR CHAINED METHOD CALLS LIKE "Minecraft.getMinecraft()" + Do we resolve this by increasing METHOD_CALL PRIORITY or how exactly? + Think of the best fitting solution that doesnt regress other parts of the whole framework. + */ + } else { + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); + currentType = null; // Can't continue resolving + } + } else { + // Can't resolve - mark as undefined + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); + currentType = null; + } + } + } + } + private void markImportedClassUsages(List marks) { // Find uppercase identifiers followed by dot (static method calls, field access) Pattern classUsage = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\s*\\."); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java index 2f91597d2..50a74679b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -29,7 +29,7 @@ public enum TokenType { METHOD_CALL(0x55FF55, 50), // method calls (bright green) // Variables and fields - UNDEFINED_VAR(0xAA0000, 105), // unresolved variables (dark red) - high priority + UNDEFINED_VAR(0xAA0000, 20), // unresolved variables (dark red) - high priority PARAMETER(0x5555FF, 36), // method parameters (blue) GLOBAL_FIELD(0x55FFFF, 35), // class-level fields (aqua) LOCAL_FIELD(0xFFFF55, 25), // local variables (yellow) From 457a8aae3707220ae720f8edd24129fd74427d0a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 14:07:45 +0200 Subject: [PATCH 005/337] A lot of ScriptDocument processing improvements --- .../script/interpreter/ScriptDocument.java | 288 ++++++++++++++---- 1 file changed, 225 insertions(+), 63 deletions(-) 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 f46a7fb13..8cbe9b96c 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 @@ -51,7 +51,7 @@ public class ScriptDocument { private static final Pattern METHOD_CALL_PATTERN = Pattern.compile( "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); private static final Pattern FIELD_DECL_PATTERN = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + "\\b([A-Za-z_][a-zA-Z0-9_<>,\\s\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); // Function parameters (for JS-style scripts) @@ -386,8 +386,9 @@ private void parseLocalVariables() { String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length())); // Pattern for local variable declarations: Type varName = or Type varName; + // Allows capital var names like "Minecraft Capital = new Minecraft();" Pattern localDecl = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;|,)"); + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(=|;|,)"); Matcher m = localDecl.matcher(bodyText); while (m.find()) { int absPos = bodyStart + m.start(); @@ -876,70 +877,211 @@ private void markMethodCalls(List marks) { if (isExcluded(start)) continue; - // Check if this is preceded by a dot (method call on an object) - boolean hasReceiver = false; - String receiverName = null; - int scanPos = start - 1; - while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos))) - scanPos--; - - if (scanPos >= 0 && text.charAt(scanPos) == '.') { - hasReceiver = true; - // Find receiver identifier - int recvEnd = scanPos - 1; - while (recvEnd >= 0 && Character.isWhitespace(text.charAt(recvEnd))) - recvEnd--; - int recvStart = recvEnd; - while (recvStart >= 0 && Character.isJavaIdentifierPart(text.charAt(recvStart))) - recvStart--; - recvStart++; - if (recvStart <= recvEnd && recvStart >= 0) { - receiverName = text.substring(recvStart, recvEnd + 1); + // Skip if in import/package statement + if (isInImportOrPackage(start)) + continue; + + // Resolve the receiver chain and get the final type + TypeInfo receiverType = resolveReceiverChain(start); + Object wrongArg; + + if (receiverType != null) { + // We have a resolved receiver type - check if method exists + if (receiverType.hasMethod(methodName)) { + MethodInfo methodInfo = receiverType.getMethodInfo(methodName); + // TODO: Add argument validation here in future + marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo)); + } else { + // Method doesn't exist on this type + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); } - } + } else { + // No receiver OR receiver couldn't be resolved + // Check if there's a dot before this (meaning there WAS a receiver, we just couldn't resolve it) + boolean hasDot = isPrecededByDot(start); - if (hasReceiver && receiverName != null && !receiverName.isEmpty()) { - // Check if receiver is an uppercase class reference (static call) - if (Character.isUpperCase(receiverName.charAt(0))) { - TypeInfo recvType = resolveType(receiverName); - if (recvType == null || !recvType.isResolved()) { - // Method call on unresolved type - mark as undefined - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); - continue; - } - // Check if method exists on resolved type - if (!recvType.hasMethod(methodName) ) { + if (hasDot) { + // There was a receiver but we couldn't resolve it - mark as undefined + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + } else { + // No receiver - standalone method call + // Check if this method is defined in the script itself + if (isScriptMethod(methodName)) { + // TODO: Add argument validation here in future + + marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); + } else { + // Unknown standalone method - mark as undefined marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); - continue; } - // Valid method call on static type - MethodInfo methodInfo = recvType.getMethodInfo(methodName); - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo)); - } else { - // Lower-case receiver - this is a method call on an instance - // Try to resolve the receiver's type - - //TODO IMPROVE HERE: currently only resolves global fields, not chained fields or 'this' - FieldInfo receiverField = resolveVariable(receiverName, start); - if (receiverField != null && receiverField.getTypeInfo() != null) { - TypeInfo recvType = receiverField.getTypeInfo(); - if (recvType.hasMethod(methodName)) { - MethodInfo methodInfo = recvType.getMethodInfo(methodName); - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo)); + } + } + } + } + + static { + int x = 5; + int y = 10; + Minecraft mc = null; + + //Type 1: + // foo(x,y); + + //Type 2: + // foo(x,y,mc); + } + + public static int foo(int x, int y, Boolean z) { + return x + 10; + } + + /** + * Resolve the full receiver chain before a method call position. + * For example, for "mc.thePlayer.worldObj.weatherEffects.get()", + * this would resolve mc -> Minecraft -> thePlayer -> EntityPlayer -> worldObj -> World -> weatherEffects -> List + * and return the final TypeInfo (List). + * + * @param methodNameStart The start position of the method name + * @return The TypeInfo of the final receiver, or null if no receiver or couldn't resolve + */ + private TypeInfo resolveReceiverChain(int methodNameStart) { + // First check if preceded by a dot + int scanPos = methodNameStart - 1; + while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos))) + scanPos--; + + if (scanPos < 0 || text.charAt(scanPos) != '.') { + return null; // No receiver + } + + // Walk backward to collect all identifiers in the chain + List chainSegments = new ArrayList<>(); + List segmentPositions = new ArrayList<>(); + + int pos = scanPos; // Currently at the dot + + while (pos >= 0) { + // Skip the dot + pos--; + + // Skip whitespace + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + if (pos < 0) + break; + + // Check if this is the end of an identifier + if (!Character.isJavaIdentifierPart(text.charAt(pos))) { + // Could be ')' from a method call - we need to skip that + if (text.charAt(pos) == ')') { + // This is a method call result, e.g., getList().get() + // For now, we can't resolve method return types in chains + // So we return null to indicate unresolvable + return null; + } + break; + } + + // Read the identifier backward + int identEnd = pos + 1; + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) + pos--; + int identStart = pos + 1; + + String ident = text.substring(identStart, identEnd); + chainSegments.add(0, ident); // Add to front (we're going backward) + segmentPositions.add(0, new int[]{identStart, identEnd}); + + // Skip whitespace + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + // Check if there's another dot (continuing the chain) + if (pos >= 0 && text.charAt(pos) == '.') { + // Continue the loop to get the next segment + continue; + } else { + // No more dots - end of chain + break; + } + } + + if (chainSegments.isEmpty()) { + return null; + } + + // Now resolve the chain from left to right + String firstSegment = chainSegments.get(0); + TypeInfo currentType = null; + + if (firstSegment.equals("this")) { + // For 'this', we use globalFields for resolution + // Can't fully resolve 'this' without class context, but we can try + currentType = null; + + if (chainSegments.size() > 1) { + // this.something - check globalFields + String nextField = chainSegments.get(1); + if (globalFields.containsKey(nextField)) { + currentType = globalFields.get(nextField).getTypeInfo(); + // Continue resolving from index 2 + for (int i = 2; i < chainSegments.size(); i++) { + if (currentType == null || !currentType.isResolved()) + return null; + String field = chainSegments.get(i); + if (currentType.hasField(field)) { + FieldInfo fieldInfo = currentType.getFieldInfo(field); + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; } else { - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + return null; } - } else { - // TODO weatherEffects.gest() is marked valid here, despite List having no gest() method - // Can't resolve receiver - mark method call anyway (could be valid at runtime) - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); } + return currentType; } + } + return null; + } else if (Character.isUpperCase(firstSegment.charAt(0))) { + // Static access like Minecraft.getMinecraft() - the type is the class itself + currentType = resolveType(firstSegment); + } else { + // Variable like mc.thePlayer.worldObj + int[] firstPos = segmentPositions.get(0); + FieldInfo varInfo = resolveVariable(firstSegment, firstPos[0]); + if (varInfo != null) { + currentType = varInfo.getTypeInfo(); + } + } + + // Resolve remaining segments + for (int i = 1; i < chainSegments.size(); i++) { + if (currentType == null || !currentType.isResolved()) { + return null; + } + + String field = chainSegments.get(i); + if (currentType.hasField(field)) { + FieldInfo fieldInfo = currentType.getFieldInfo(field); + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; } else { - // No receiver - standalone method call (local function or global) - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); + // Field doesn't exist + return null; } } + + return currentType; + } + + /** + * Check if a method name is defined in this script. + */ + private boolean isScriptMethod(String methodName) { + for (MethodInfo method : methods) { + if (method.getName().equals(methodName)) { + return true; + } + } + return false; } private void markVariables(List marks) { @@ -982,10 +1124,6 @@ private void markVariables(List marks) { if (isInImportOrPackage(position)) continue; - // Skip uppercase (type references handled elsewhere) - if (Character.isUpperCase(name.charAt(0))) - continue; - // Skip method calls (followed by paren) if (isFollowedByParen(m.end(1))) continue; @@ -993,6 +1131,10 @@ private void markVariables(List marks) { // Find containing method MethodInfo containingMethod = findMethodAtPosition(position); + // For uppercase identifiers, only process if it's a known field + // Otherwise, let type handling (markImportedClassUsages) handle it + boolean isUppercase = Character.isUpperCase(name.charAt(0)); + if (containingMethod != null) { // Check parameters if (containingMethod.hasParameter(name)) { @@ -1019,6 +1161,10 @@ private void markVariables(List marks) { continue; } + // Skip uppercase if not a known field - type handling will deal with it + if (isUppercase) + continue; + // Unknown variable inside method - mark as undefined marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); } else { @@ -1030,6 +1176,10 @@ private void markVariables(List marks) { continue; } + // Skip uppercase if not a known field - type handling will deal with it + if (isUppercase) + continue; + // Unknown variable outside method - mark as undefined marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); } @@ -1039,6 +1189,7 @@ private void markVariables(List marks) { /** * Mark chained field accesses like: mc.player.world, array.length, this.field, etc. * This handles dot-separated access chains and colors each segment appropriately. + * Does NOT mark method calls (identifiers followed by parentheses) - those are handled by markMethodCalls. */ private void markChainedFieldAccesses(List marks) { // Pattern to find identifier chains: identifier.identifier, this.identifier, etc. @@ -1056,6 +1207,17 @@ private void markChainedFieldAccesses(List marks) { if (isInImportOrPackage(chainStart)) continue; + // Check if the SECOND segment is a method call (followed by parentheses) + // If so, we skip this match entirely - markMethodCalls will handle it + int afterSecond = m.end(2); + int checkPos = afterSecond; + while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos))) + checkPos++; + if (checkPos < text.length() && text.charAt(checkPos) == '(') { + // This is "something.methodCall()" - skip, handled by markMethodCalls + continue; + } + // Build the full chain by continuing to match subsequent .identifier patterns List chainSegments = new ArrayList<>(); List segmentPositions = new ArrayList<>(); // [start, end] for each segment @@ -1093,10 +1255,10 @@ private void markChainedFieldAccesses(List marks) { int identEnd = pos; // Check if this is followed by parentheses (method call - stop) - int checkPos = pos; - while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos))) - checkPos++; - if (checkPos < text.length() && text.charAt(checkPos) == '(') { + int checkPosInner = pos; + while (checkPosInner < text.length() && Character.isWhitespace(text.charAt(checkPosInner))) + checkPosInner++; + if (checkPosInner < text.length() && text.charAt(checkPosInner) == '(') { break; // This is a method call, not a field } From 9cb4c96eb3810e3510c8d88769b47db5dae1dab0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 19:10:37 +0200 Subject: [PATCH 006/337] Code logic improvements --- .../util/script/interpreter/ImportData.java | 68 +- .../script/interpreter/MethodCallInfo.java | 395 +++++++ .../util/script/interpreter/MethodInfo.java | 21 +- .../script/interpreter/ScriptDocument.java | 989 ++++++++++++++++-- .../util/script/interpreter/ScriptLine.java | 157 ++- .../script/interpreter/ScriptTypeInfo.java | 187 ++++ .../gui/util/script/interpreter/Token.java | 14 + .../util/script/interpreter/TokenType.java | 4 + .../gui/util/script/interpreter/TypeInfo.java | 36 +- 9 files changed, 1769 insertions(+), 102 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java index 00557fa6a..d3fe10820 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java @@ -1,8 +1,12 @@ package noppes.npcs.client.gui.util.script.interpreter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Represents a single import statement with all its metadata. - * Tracks the full import path, resolution status, and source positions. + * Tracks the full import path, resolution status, source positions, and references. */ public final class ImportData { @@ -17,6 +21,12 @@ public final class ImportData { private TypeInfo resolvedType; // null if wildcard or unresolved private boolean resolved; // whether resolution was attempted and succeeded + + // Track usage count (simpler than tracking individual tokens) + private int usageCount = 0; + + // Track all tokens that reference this import (optional, for detailed analysis) + private final List referencingTokens = new ArrayList<>(); public ImportData(String fullPath, String simpleName, boolean isWildcard, boolean isStatic, int startOffset, int endOffset, int pathStartOffset, int pathEndOffset) { @@ -57,6 +67,62 @@ public void setResolvedType(TypeInfo typeInfo) { public void markResolved(boolean resolved) { this.resolved = resolved; } + + // ==================== REFERENCE TRACKING ==================== + + /** + * Increment the usage count for this import. + * Called during type resolution when this import is used. + */ + public void incrementUsage() { + usageCount++; + } + + /** + * Add a token that references this import. + * Called when a token is created that uses this import's type. + */ + public void addReference(Token token) { + if (token != null && !referencingTokens.contains(token)) { + referencingTokens.add(token); + } + } + + /** + * Get all tokens that reference this import. + */ + public List getReferencingTokens() { + return Collections.unmodifiableList(referencingTokens); + } + + /** + * Check if this import is used (has any references or usages). + */ + public boolean isUsed() { + return usageCount > 0 || !referencingTokens.isEmpty(); + } + + /** + * Get the usage count for this import. + */ + public int getUsageCount() { + return usageCount; + } + + /** + * Get the number of references to this import. + */ + public int getReferenceCount() { + return referencingTokens.size(); + } + + /** + * Clear all references and reset usage count (used when re-tokenizing). + */ + public void clearReferences() { + referencingTokens.clear(); + usageCount = 0; + } /** * Get the package portion of the import path. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java new file mode 100644 index 000000000..79b7198a5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -0,0 +1,395 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import scala.annotation.meta.param; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Stores information about a method call for argument validation. + * This includes the method name, the arguments passed, and validation results. + */ +public class MethodCallInfo { + + /** + * Represents a single argument in a method call. + */ + public static class Argument { + private final String text; // The text of the argument expression + private final int startOffset; // Start position in source + private final int endOffset; // End position in source + private final TypeInfo resolvedType; // The resolved type of the argument (null if unresolved) + private final boolean valid; // Whether this arg matches the expected parameter type + private final String errorMessage; // Error message if invalid + + public Argument(String text, int startOffset, int endOffset, TypeInfo resolvedType, + boolean valid, String errorMessage) { + this.text = text; + this.startOffset = startOffset; + this.endOffset = endOffset; + this.resolvedType = resolvedType; + this.valid = valid; + this.errorMessage = errorMessage; + } + + public String getText() { + return text; + } + + public int getStartOffset() { + return startOffset; + } + + public int getEndOffset() { + return endOffset; + } + + public TypeInfo getResolvedType() { + return resolvedType; + } + + public boolean isValid() { + return valid; + } + + public String getErrorMessage() { + return errorMessage; + } + + public boolean equals(Token t) { + return text.equals(t.getText()) && + startOffset == t.getGlobalStart() && + endOffset == t.getGlobalEnd(); + } + + @Override + public String toString() { + return "Arg{" + text + " [" + startOffset + "-" + endOffset + "]" + + (resolvedType != null ? " :" + resolvedType.getSimpleName() : "") + + (valid ? "" : " INVALID: " + errorMessage) + "}"; + } + } + + /** + * Validation error type. + */ + public enum ErrorType { + NONE, + WRONG_ARG_COUNT, // Number of args doesn't match any overload + WRONG_ARG_TYPE, // Specific argument has wrong type + STATIC_ACCESS_ERROR, // Trying to call instance method statically or vice versa + UNRESOLVED_METHOD, // Method doesn't exist + UNRESOLVED_RECEIVER // Can't resolve the receiver type + } + + private final String methodName; + private final int methodNameStart; // Position of method name + private final int methodNameEnd; + private final int openParenOffset; // Position of '(' + private final int closeParenOffset; // Position of ')' + private final List arguments; + private final TypeInfo receiverType; // The type on which this method is called (null for standalone) + private final MethodInfo resolvedMethod; // The resolved method (null if unresolved) + private final boolean isStaticAccess; // True if this is Class.method() style access + + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + private int errorArgIndex = -1; // Index of the problematic argument (for WRONG_ARG_TYPE) + private List argumentTypeErrors = new ArrayList<>(); + + public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd, + int openParenOffset, int closeParenOffset, + List arguments, TypeInfo receiverType, + MethodInfo resolvedMethod) { + this(methodName, methodNameStart, methodNameEnd, openParenOffset, closeParenOffset, + arguments, receiverType, resolvedMethod, false); + } + + public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd, + int openParenOffset, int closeParenOffset, + List arguments, TypeInfo receiverType, + MethodInfo resolvedMethod, boolean isStaticAccess) { + this.methodName = methodName; + this.methodNameStart = methodNameStart; + this.methodNameEnd = methodNameEnd; + this.openParenOffset = openParenOffset; + this.closeParenOffset = closeParenOffset; + this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>(); + this.receiverType = receiverType; + this.resolvedMethod = resolvedMethod; + this.isStaticAccess = isStaticAccess; + } + + // Getters + public String getMethodName() { + return methodName; + } + + public int getMethodNameStart() { + return methodNameStart; + } + + public int getMethodNameEnd() { + return methodNameEnd; + } + + public int getOpenParenOffset() { + return openParenOffset; + } + + public int getCloseParenOffset() { + return closeParenOffset; + } + + public List getArguments() { + return Collections.unmodifiableList(arguments); + } + + public int getArgumentCount() { + return arguments.size(); + } + + public TypeInfo getReceiverType() { + return receiverType; + } + + public MethodInfo getResolvedMethod() { + return resolvedMethod; + } + + public boolean isStaticAccess() { + return isStaticAccess; + } + + public ErrorType getErrorType() { + return errorType; + } + + public String getErrorMessage() { + return errorMessage; + } + + /** + * Get the full span of the method call including parentheses. + * Used for underlining the entire call on arg count errors. + */ + public int getFullCallStart() { + return methodNameStart; + } + + public int getFullCallEnd() { + return closeParenOffset + 1; + } + + /** + * Check if this method call has any validation errors. + */ + public boolean hasError() { + return errorType != ErrorType.NONE; + } + + /** + * Check if this is an arg count error (underline whole call). + */ + public boolean hasArgCountError() { + return errorType == ErrorType.WRONG_ARG_COUNT; + } + + /** + * Check if this is an arg type error (underline specific arg). + */ + public boolean hasArgTypeError() { + return !this.argumentTypeErrors.isEmpty(); + } + public boolean hasArgTypeError(Token t) { + for (ArgumentTypeError ate : this.argumentTypeErrors) { + if (ate.getArg().equals(t)) { + return true; + } + } + return false; + } + + /** + * Check if this is a static access error (underline method name). + */ + public boolean hasStaticAccessError() { + return errorType == ErrorType.STATIC_ACCESS_ERROR; + } + + // Setters for validation results + public void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + public void setArgTypeError(int argIndex, String message) { + this.argumentTypeErrors.add(new ArgumentTypeError(arguments.get(argIndex), argIndex, message)); + } + + + public class ArgumentTypeError { + private ErrorType type = ErrorType.WRONG_ARG_TYPE; + private final Argument arg; + private final int argIndex; + private final String message; + + public ArgumentTypeError(Argument arg, int argIndex, String message) { + this.arg = arg; + this.argIndex = argIndex; + this.message = message; + } + + public int getArgIndex() { + return argIndex; + } + + public String getMessage() { + return message; + } + + public Argument getArg() { + return arg; + } + } + + /** + * Validate this method call against the resolved method signature. + * Sets error information if validation fails. + */ + public void validate() { + if (resolvedMethod == null) { + setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve method '" + methodName + "'"); + return; + } + + // Check static/instance access + if (isStaticAccess && !resolvedMethod.isStatic()) { + setError(ErrorType.STATIC_ACCESS_ERROR, + "Cannot call instance method '" + methodName + "' on a class type"); + return; + } + + List params = resolvedMethod.getParameters(); + int expectedCount = params.size(); + int actualCount = arguments.size(); + + // Check arg count + if (actualCount != expectedCount) { + setError(ErrorType.WRONG_ARG_COUNT, + "Expected " + expectedCount + " argument(s) but got " + actualCount); + return; + } + + // Check each argument type + for (int i = 0; i < actualCount; i++) { + Argument arg = arguments.get(i); + FieldInfo para = params.get(i); + + TypeInfo argType = arg.getResolvedType(); + TypeInfo paramType = para.getDeclaredType(); + if (argType != null && paramType != null) { + if (!isTypeCompatible(argType, paramType)) { + setArgTypeError(i, "Expected " + paramType.getSimpleName() + + " but got " + argType.getSimpleName()); + return; + } + } else if (paramType == null) { + setArgTypeError(i, "Parameter type of '" + para.getName() + "' is unresolved"); + return; + } else if (argType == null) { + setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'"); + return; + } + } + } + + /** + * Check if sourceType can be assigned to targetType. + * Handles primitive widening, inheritance, etc. + */ + private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { + if (sourceType == null || targetType == null) { + return true; // Can't verify, assume compatible + } + + if (sourceType.equals(targetType)) { + return true; + } + + Class sourceClass = sourceType.getJavaClass(); + Class targetClass = targetType.getJavaClass(); + + if (sourceClass == null || targetClass == null) { + return true; // Can't verify, assume compatible + } + + // Check inheritance + if (targetClass.isAssignableFrom(sourceClass)) { + return true; + } + + // Check primitive widening conversions + if (isPrimitiveWidening(sourceClass, targetClass)) { + return true; + } + + return false; + } + + /** + * Check if widening conversion is valid between primitives. + */ + private boolean isPrimitiveWidening(Class source, Class target) { + // byte -> short -> int -> long -> float -> double + // char -> int -> long -> float -> double + if (target == double.class || target == Double.class) { + return source == float.class || source == Float.class || + source == long.class || source == Long.class || + source == int.class || source == Integer.class || + source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == float.class || target == Float.class) { + return source == long.class || source == Long.class || + source == int.class || source == Integer.class || + source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == long.class || target == Long.class) { + return source == int.class || source == Integer.class || + source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == int.class || target == Integer.class) { + return source == short.class || source == Short.class || + source == byte.class || source == Byte.class || + source == char.class || source == Character.class; + } + if (target == short.class || target == Short.class) { + return source == byte.class || source == Byte.class; + } + return false; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("MethodCallInfo{"); + sb.append(methodName).append("("); + for (int i = 0; i < arguments.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(arguments.get(i).getText()); + } + sb.append(")"); + if (hasError()) { + sb.append(" ERROR: ").append(errorType).append(" - ").append(errorMessage); + } + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 58f229797..93f393c40 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -19,10 +20,12 @@ public final class MethodInfo { private final int bodyEnd; // End of method body (before }) private final boolean resolved; private final boolean isDeclaration; // true if this is a declaration, false if it's a call + private final boolean isStatic; // true if this is a static method private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, List parameters, int declarationOffset, - int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration) { + int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration, + boolean isStatic) { this.name = name; this.returnType = returnType; this.containingType = containingType; @@ -32,12 +35,18 @@ private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, this.bodyEnd = bodyEnd; this.resolved = resolved; this.isDeclaration = isDeclaration; + this.isStatic = isStatic; } // Factory methods public static MethodInfo declaration(String name, TypeInfo returnType, List params, int declOffset, int bodyStart, int bodyEnd) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true); + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, false); + } + + public static MethodInfo declaration(String name, TypeInfo returnType, List params, + int declOffset, int bodyStart, int bodyEnd, boolean isStatic) { + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, isStatic); } public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { @@ -47,7 +56,7 @@ public static MethodInfo call(String name, TypeInfo containingType, int paramCou for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false); + return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false, false); } public static MethodInfo unresolvedCall(String name, int paramCount) { @@ -55,7 +64,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, null, params, -1, -1, -1, false, false); + return new MethodInfo(name, null, null, params, -1, -1, -1, false, false, false); } /** @@ -65,6 +74,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInfo containingType) { String name = method.getName(); TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); + boolean isStatic = Modifier.isStatic(method.getModifiers()); List params = new ArrayList<>(); Class[] paramTypes = method.getParameterTypes(); @@ -73,7 +83,7 @@ public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInf params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, isStatic); } // Getters @@ -88,6 +98,7 @@ public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInf public boolean isResolved() { return resolved; } public boolean isDeclaration() { return isDeclaration; } public boolean isCall() { return !isDeclaration; } + public boolean isStatic() { return isStatic; } /** * Check if a position is inside this method's body. 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 8cbe9b96c..e5ecc117a 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 @@ -68,6 +68,9 @@ public class ScriptDocument { private final Set wildcardPackages = new HashSet<>(); private Map importsBySimpleName = new HashMap<>(); + // Script-defined types (classes, interfaces, enums defined in the script) + private final Map scriptTypes = new HashMap<>(); + // Local variables per method (methodStartOffset -> {varName -> FieldInfo}) private final Map> methodLocals = new HashMap<>(); @@ -176,6 +179,7 @@ public void formatCodeText() { wildcardPackages.clear(); excludedRanges.clear(); methodLocals.clear(); + scriptTypes.clear(); // Phase 1: Find excluded regions (strings/comments) findExcludedRanges(); @@ -183,7 +187,7 @@ public void formatCodeText() { // Phase 2: Parse imports parseImports(); - // Phase 3: Parse structure (methods, fields, locals) + // Phase 3: Parse structure (script types, methods, fields, locals) parseStructure(); // Phase 4: Build marks and assign to lines @@ -291,6 +295,14 @@ private void parseImports() { // ==================== PHASE 3: STRUCTURE ==================== private void parseStructure() { + // Clear import references before re-parsing + for (ImportData imp : imports) { + imp.clearReferences(); + } + + // Parse script-defined types (classes, interfaces, enums) + parseScriptTypes(); + // Parse methods parseMethodDeclarations(); @@ -301,6 +313,141 @@ private void parseStructure() { parseGlobalFields(); } + /** + * Parse class, interface, and enum declarations defined in the script. + * Creates ScriptTypeInfo instances and stores them for later resolution. + */ + private void parseScriptTypes() { + // Pattern: (class|interface|enum) ClassName { ... } + Pattern typeDecl = Pattern.compile( + "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:extends\\s+[A-Za-z_][a-zA-Z0-9_.]*)?\\s*(?:implements\\s+[A-Za-z_][a-zA-Z0-9_.,\\s]*)?\\s*\\{"); + + Matcher m = typeDecl.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; + + String kindStr = m.group(1); + String typeName = m.group(2); + int bodyStart = text.indexOf('{', m.start()); + int bodyEnd = findMatchingBrace(bodyStart); + + if (bodyEnd < 0) { + bodyEnd = text.length(); + } + + TypeInfo.Kind kind; + switch (kindStr) { + case "interface": kind = TypeInfo.Kind.INTERFACE; break; + case "enum": kind = TypeInfo.Kind.ENUM; break; + default: kind = TypeInfo.Kind.CLASS; break; + } + + ScriptTypeInfo scriptType = ScriptTypeInfo.create( + typeName, kind, m.start(), bodyStart, bodyEnd); + + // Parse fields and methods inside this type + parseScriptTypeMembers(scriptType); + + scriptTypes.put(typeName, scriptType); + } + } + + /** + * Parse fields and methods inside a script-defined type. + */ + private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { + int bodyStart = scriptType.getBodyStart(); + int bodyEnd = scriptType.getBodyEnd(); + + if (bodyStart < 0 || bodyEnd <= bodyStart) return; + + String bodyText = text.substring(bodyStart + 1, Math.min(bodyEnd, text.length())); + + // Parse field declarations + Pattern fieldPattern = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>,\\s\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + Matcher fm = fieldPattern.matcher(bodyText); + while (fm.find()) { + int absPos = bodyStart + 1 + fm.start(); + if (isExcluded(absPos)) continue; + + String typeName = fm.group(1).trim(); + String fieldName = fm.group(2); + + // Skip if this looks like a method declaration + if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || + typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || + typeName.equals("new") || typeName.equals("throw")) { + continue; + } + + // Skip if inside a nested method body + if (isInsideNestedMethod(absPos, bodyStart, bodyEnd)) { + continue; + } + + TypeInfo fieldType = resolveType(typeName); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos); + scriptType.addField(fieldInfo); + } + + // Parse method declarations + Pattern methodPattern = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); + Matcher mm = methodPattern.matcher(bodyText); + while (mm.find()) { + int absPos = bodyStart + 1 + mm.start(); + if (isExcluded(absPos)) continue; + + String returnTypeName = mm.group(1); + String methodName = mm.group(2); + String paramList = mm.group(3); + + // Skip class/interface/enum keywords + if (returnTypeName.equals("class") || returnTypeName.equals("interface") || + returnTypeName.equals("enum")) { + continue; + } + + int methodBodyStart = bodyStart + 1 + mm.end() - 1; + int methodBodyEnd = findMatchingBrace(methodBodyStart); + if (methodBodyEnd < 0) methodBodyEnd = bodyEnd; + + TypeInfo returnType = resolveType(returnTypeName); + List params = parseParametersWithPositions(paramList, + bodyStart + 1 + mm.start(3)); + + MethodInfo methodInfo = MethodInfo.declaration( + methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd); + scriptType.addMethod(methodInfo); + } + } + + /** + * Check if a position is inside a nested method body within a class. + * This prevents field declarations inside methods from being treated as class fields. + */ + private boolean isInsideNestedMethod(int position, int classBodyStart, int classBodyEnd) { + String bodyText = text.substring(classBodyStart + 1, Math.min(classBodyEnd, text.length())); + int relativePos = position - classBodyStart - 1; + + Pattern methodPattern = Pattern.compile( + "\\b[A-Za-z_][a-zA-Z0-9_<>\\[\\]]*\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*\\([^)]*\\)\\s*\\{"); + Matcher m = methodPattern.matcher(bodyText); + + while (m.find()) { + int methodBodyStart = m.end() - 1; + int absMethodBodyStart = classBodyStart + 1 + methodBodyStart; + int absMethodBodyEnd = findMatchingBrace(absMethodBodyStart); + + if (absMethodBodyEnd > 0 && position > absMethodBodyStart && position < absMethodBodyEnd) { + return true; + } + } + return false; + } + private void parseMethodDeclarations() { Pattern methodWithBody = Pattern.compile( "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); @@ -396,6 +543,7 @@ private void parseLocalVariables() { String typeName = m.group(1); String varName = m.group(2); + String delimiter = m.group(3); // Skip if it looks like a method call or control flow if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || @@ -404,13 +552,205 @@ private void parseLocalVariables() { continue; } - TypeInfo typeInfo = resolveType(typeName); + TypeInfo typeInfo; + + // For var/let/const, infer type from the right-hand side expression + if ((typeName.equals("var") || typeName.equals("let") || typeName.equals("const")) + && delimiter.equals("=")) { + // Find the right-hand side expression + int rhsStart = bodyStart + m.end(); + typeInfo = inferTypeFromExpression(rhsStart); + } else { + typeInfo = resolveType(typeName); + } + int declPos = bodyStart + m.start(2); FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method); locals.put(varName, fieldInfo); } } } + + /** + * Infer the type of an expression starting at the given position. + * Handles patterns like: + * - "new TypeName()" - returns TypeName + * - "receiver.fieldOrMethod" - resolves chain and returns result type + * - "variable" - looks up variable type + * - Literals like numbers, strings, booleans + */ + private TypeInfo inferTypeFromExpression(int position) { + // Skip whitespace + while (position < text.length() && Character.isWhitespace(text.charAt(position))) + position++; + + if (position >= text.length()) + return null; + + // Check for 'new' keyword + if (text.startsWith("new ", position)) { + position += 4; + while (position < text.length() && Character.isWhitespace(text.charAt(position))) + position++; + + // Read the type name + int typeStart = position; + while (position < text.length() && Character.isJavaIdentifierPart(text.charAt(position))) + position++; + + if (position > typeStart) { + String typeName = text.substring(typeStart, position); + return resolveType(typeName); + } + return null; + } + + // Check for string literal + if (position < text.length() && (text.charAt(position) == '"' || text.charAt(position) == '\'')) { + return resolveType("String"); + } + + // Check for numeric literal + if (position < text.length() && Character.isDigit(text.charAt(position))) { + // Check for float/double (has decimal point or f/d suffix) + int numEnd = position; + boolean hasDecimal = false; + while (numEnd < text.length() && (Character.isDigit(text.charAt(numEnd)) || + text.charAt(numEnd) == '.' || text.charAt(numEnd) == 'f' || + text.charAt(numEnd) == 'd' || text.charAt(numEnd) == 'F' || + text.charAt(numEnd) == 'D' || text.charAt(numEnd) == 'L' || + text.charAt(numEnd) == 'l')) { + if (text.charAt(numEnd) == '.') hasDecimal = true; + numEnd++; + } + String num = text.substring(position, numEnd).toLowerCase(); + if (num.endsWith("f")) return resolveType("float"); + if (num.endsWith("d") || hasDecimal) return resolveType("double"); + if (num.endsWith("l")) return resolveType("long"); + return resolveType("int"); + } + + // Check for boolean literals + if (text.startsWith("true", position) || text.startsWith("false", position)) { + return resolveType("boolean"); + } + + // Check for null + if (text.startsWith("null", position)) { + return null; // Can't infer type from null + } + + // Must be an identifier or chain - read the first identifier + if (Character.isJavaIdentifierStart(text.charAt(position))) { + int identStart = position; + while (position < text.length() && Character.isJavaIdentifierPart(text.charAt(position))) + position++; + String ident = text.substring(identStart, position); + + // Skip whitespace + while (position < text.length() && Character.isWhitespace(text.charAt(position))) + position++; + + // Check if this is the start of a chain (followed by .) + if (position < text.length() && text.charAt(position) == '.') { + // This is a chain like "event.player" or "Minecraft.getMinecraft()" + // We need to resolve the entire chain to get the final type + return inferChainType(ident, identStart, position); + } + + // Check if this is a method call (followed by ()) + if (position < text.length() && text.charAt(position) == '(') { + // Method call - check if it's a script method + if (isScriptMethod(ident)) { + MethodInfo methodInfo = getScriptMethodInfo(ident); + return (methodInfo != null) ? methodInfo.getReturnType() : null; + } + return null; + } + + // Just a variable - resolve its type + FieldInfo varInfo = resolveVariable(ident, identStart); + return (varInfo != null) ? varInfo.getTypeInfo() : null; + } + + return null; + } + + /** + * Infer the type from a chain expression starting with the given identifier. + */ + private TypeInfo inferChainType(String firstIdent, int identStart, int dotPosition) { + TypeInfo currentType = null; + + // Resolve the first segment + if (Character.isUpperCase(firstIdent.charAt(0))) { + // Static access like Event.player + currentType = resolveType(firstIdent); + } else { + // Variable access + FieldInfo varInfo = resolveVariable(firstIdent, identStart); + currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; + } + + if (currentType == null || !currentType.isResolved()) + return null; + + // Now resolve the rest of the chain + int pos = dotPosition; + while (pos < text.length() && text.charAt(pos) == '.') { + pos++; // Skip the dot + + // Skip whitespace + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + + if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) + break; + + // Read the next identifier + int segStart = pos; + while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) + pos++; + String segment = text.substring(segStart, pos); + + // Skip whitespace + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + + // Check if this is a method call + if (pos < text.length() && text.charAt(pos) == '(') { + // Method call + if (currentType.hasMethod(segment)) { + MethodInfo methodInfo = currentType.getMethodInfo(segment); + currentType = (methodInfo != null) ? methodInfo.getReturnType() : null; + } else { + return null; + } + + // Skip to after the closing paren + int closeParen = findMatchingParen(pos); + if (closeParen < 0) return null; + pos = closeParen + 1; + } else { + // Field access + if (currentType.hasField(segment)) { + FieldInfo fieldInfo = currentType.getFieldInfo(segment); + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } else { + return null; + } + } + + if (currentType == null || !currentType.isResolved()) + return null; + + // Skip whitespace for next iteration + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + } + + return currentType; + } private void parseGlobalFields() { Matcher m = FIELD_DECL_PATTERN.matcher(text); @@ -440,6 +780,14 @@ private void parseGlobalFields() { } private TypeInfo resolveType(String typeName) { + return resolveTypeAndTrackUsage(typeName); + } + + /** + * Resolve a type and track the import usage for unused import detection. + * Checks script-defined types first, then falls back to imported types. + */ + private TypeInfo resolveTypeAndTrackUsage(String typeName) { if (typeName == null || typeName.isEmpty()) return null; @@ -450,8 +798,35 @@ private TypeInfo resolveType(String typeName) { // Strip array brackets baseName = baseName.replace("[]", "").trim(); - // Try to resolve - return typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + // Check script-defined types FIRST + if (scriptTypes.containsKey(baseName)) { + return scriptTypes.get(baseName); + } + + // Check if there's an explicit import for this type + ImportData usedImport = importsBySimpleName.get(baseName); + + // Try to resolve through imports/classpath + TypeInfo result = typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + + // Track the import usage if we found one and the resolution succeeded + if (result != null && usedImport != null) { + usedImport.incrementUsage(); + } + + // Also check wildcard imports if the type was resolved through one + if (result != null && usedImport == null && wildcardPackages != null) { + String resultPkg = result.getPackageName(); + // Check if resolved through a wildcard + for (ImportData imp : imports) { + if (imp.isWildcard() && resultPkg != null && resultPkg.equals(imp.getFullPath())) { + imp.incrementUsage(); + break; + } + } + } + + return result; } // ==================== PHASE 4: BUILD MARKS ==================== @@ -493,9 +868,27 @@ private List buildMarks() { // Imported class usages markImportedClassUsages(marks); + + // Mark unused imports (after all other marks are built) + markUnusedImports(marks); return marks; } + + /** + * Find and mark unused imports as UNUSED_IMPORT type. + * This must be called after all other mark building is complete. + */ + private void markUnusedImports(List marks) { + for (ImportData imp : imports) { + if (!imp.isUsed() && imp.isResolved() && !imp.isWildcard()) { + // This import is not used - find and update its marks + // Mark the entire import line as unused (gray color) + marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getEndOffset(), + TokenType.UNUSED_IMPORT, imp)); + } + } + } private void addPatternMarks(List marks, Pattern pattern, TokenType type) { addPatternMarks(marks, pattern, type, 0); @@ -860,6 +1253,13 @@ private void markMethodDeclarations(List marks) { if (isExcluded(m.start())) continue; + // Skip class/interface/enum declarations - these look like method declarations + // but "class Foo(" is not a method, it's a class declaration + String returnType = m.group(1); + if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum")) { + continue; + } + // Return type marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); // Method name @@ -870,69 +1270,345 @@ private void markMethodDeclarations(List marks) { private void markMethodCalls(List marks) { Matcher m = METHOD_CALL_PATTERN.matcher(text); while (m.find()) { - int start = m.start(1); - int end = m.end(1); + int nameStart = m.start(1); + int nameEnd = m.end(1); String methodName = m.group(1); - if (isExcluded(start)) + if (isExcluded(nameStart)) continue; // Skip if in import/package statement - if (isInImportOrPackage(start)) + if (isInImportOrPackage(nameStart)) + continue; + + // Find the opening parenthesis by scanning forward from the method name end + // The regex includes \( but we scan manually to be safe + int openParen = nameEnd; + while (openParen < text.length() && Character.isWhitespace(text.charAt(openParen))) { + openParen++; + } + + if (openParen >= text.length() || text.charAt(openParen) != '(') { + // Not actually a method call + continue; + } + + // Find the matching closing parenthesis + int closeParen = findMatchingParen(openParen); + if (closeParen < 0) { + // Malformed - no closing paren continue; + } + + // Parse the arguments + List arguments = parseMethodArguments(openParen + 1, closeParen); + // Check if this is a static access (Class.method() style) + boolean isStaticAccess = isStaticAccessCall(nameStart); + // Resolve the receiver chain and get the final type - TypeInfo receiverType = resolveReceiverChain(start); - Object wrongArg; + TypeInfo receiverType = resolveReceiverChain(nameStart); + MethodInfo resolvedMethod = null; if (receiverType != null) { // We have a resolved receiver type - check if method exists if (receiverType.hasMethod(methodName)) { - MethodInfo methodInfo = receiverType.getMethodInfo(methodName); - // TODO: Add argument validation here in future - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL, methodInfo)); + resolvedMethod = receiverType.getMethodInfo(methodName); + + // Create MethodCallInfo for validation (with static access flag) + MethodCallInfo callInfo = new MethodCallInfo( + methodName, nameStart, nameEnd, openParen, closeParen, + arguments, receiverType, resolvedMethod, isStaticAccess + ); + callInfo.validate(); + + // Always mark method call as green (METHOD_CALL), with error info attached for underline + // The MethodCallInfo is attached so rendering can draw curly underline if there's an error + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { // Method doesn't exist on this type - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } } else { // No receiver OR receiver couldn't be resolved // Check if there's a dot before this (meaning there WAS a receiver, we just couldn't resolve it) - boolean hasDot = isPrecededByDot(start); + boolean hasDot = isPrecededByDot(nameStart); if (hasDot) { // There was a receiver but we couldn't resolve it - mark as undefined - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } else { // No receiver - standalone method call // Check if this method is defined in the script itself if (isScriptMethod(methodName)) { - // TODO: Add argument validation here in future - - marks.add(new ScriptLine.Mark(start, end, TokenType.METHOD_CALL)); + resolvedMethod = getScriptMethodInfo(methodName); + + // Create MethodCallInfo for validation + MethodCallInfo callInfo = new MethodCallInfo( + methodName, nameStart, nameEnd, openParen, closeParen, + arguments, null, resolvedMethod + ); + callInfo.validate(); + + // Always mark method call as green (METHOD_CALL), with error info attached for underline + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { // Unknown standalone method - mark as undefined - marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR)); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } } } } } + + /** + * Check if a method call is a static access (Class.method() style). + * Returns true if the immediate receiver before the dot is a class name (uppercase). + */ + private boolean isStaticAccessCall(int methodNameStart) { + // Walk backward to find the dot + int pos = methodNameStart - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + if (pos < 0 || text.charAt(pos) != '.') + return false; + + // Skip the dot and any whitespace + pos--; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + if (pos < 0) + return false; + + // Check what's before the dot - could be: + // 1. An identifier ending (field or class name) + // 2. A closing paren (method call result) + // 3. A closing bracket (array access) + + char c = text.charAt(pos); + if (c == ')' || c == ']') { + // Method call or array - this is instance access + return false; + } + + if (!Character.isJavaIdentifierPart(c)) + return false; + + // Read the identifier backward + int identEnd = pos + 1; + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) + pos--; + int identStart = pos + 1; + + String ident = text.substring(identStart, identEnd); + + // Check if this is a class name (starts with uppercase) + // AND check if there's no dot before it (making it a direct class reference) + // If there's a chain like "obj.SomeClass.method()", we need to check further + + // Skip whitespace before the identifier + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) + pos--; + + // If preceded by a dot, this might be part of a longer chain + // In that case, it's not a direct static access + if (pos >= 0 && text.charAt(pos) == '.') { + return false; + } + + // It's static access if the identifier starts with uppercase + return !ident.isEmpty() && Character.isUpperCase(ident.charAt(0)); + } - static { - int x = 5; - int y = 10; - Minecraft mc = null; - - //Type 1: - // foo(x,y); + /** + * Find the matching closing parenthesis for an opening parenthesis. + * Handles nested parentheses, strings, and comments. + */ + private int findMatchingParen(int openPos) { + if (openPos < 0 || openPos >= text.length() || text.charAt(openPos) != '(') { + return -1; + } + + int depth = 1; + boolean inString = false; + boolean inChar = false; + char stringChar = 0; + + for (int i = openPos + 1; i < text.length(); i++) { + char c = text.charAt(i); + char prev = (i > 0) ? text.charAt(i - 1) : 0; + + // Handle string literals + if (!inChar && (c == '"' || c == '\'') && prev != '\\') { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + continue; + } + + if (inString) continue; + + // Skip excluded regions (comments) + if (isExcluded(i)) continue; + + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + return i; + } + } + } + + return -1; // No matching paren found + } - //Type 2: - // foo(x,y,mc); + /** + * Parse method arguments from the text between opening and closing parentheses. + * Handles nested expressions, strings, and complex argument types. + */ + private List parseMethodArguments(int start, int end) { + List args = new ArrayList<>(); + + if (start >= end) { + return args; // No arguments + } + + int depth = 0; + int argStart = start; + boolean inString = false; + char stringChar = 0; + + for (int i = start; i <= end; i++) { + if (i == end || (depth == 0 && !inString && text.charAt(i) == ',')) { + // End of an argument + String argText = text.substring(argStart, i).trim(); + if (!argText.isEmpty()) { + // Find the actual start/end positions (excluding leading/trailing whitespace) + int actualStart = argStart; + while (actualStart < i && Character.isWhitespace(text.charAt(actualStart))) { + actualStart++; + } + int actualEnd = i; + while (actualEnd > actualStart && Character.isWhitespace(text.charAt(actualEnd - 1))) { + actualEnd--; + } + + // Try to resolve the argument type + TypeInfo argType = resolveExpressionType(argText, actualStart); + + args.add(new MethodCallInfo.Argument( + argText, actualStart, actualEnd, argType, true, null + )); + } + argStart = i + 1; + continue; + } + + char c = text.charAt(i); + char prev = (i > start) ? text.charAt(i - 1) : 0; + + // Handle string literals + if (!inString && (c == '"' || c == '\'') && prev != '\\') { + inString = true; + stringChar = c; + } else if (inString && c == stringChar && prev != '\\') { + inString = false; + } + + if (!inString) { + if (c == '(' || c == '[' || c == '{' || c == '<') { + depth++; + } else if (c == ')' || c == ']' || c == '}' || c == '>') { + depth--; + } + } + } + + return args; } - public static int foo(int x, int y, Boolean z) { - return x + 10; + /** + * Try to resolve the type of an expression (for argument type checking). + * This is a simplified version that handles common cases. + */ + private TypeInfo resolveExpressionType(String expr, int position) { + expr = expr.trim(); + + if (expr.isEmpty()) { + return null; + } + + // String literals + if (expr.startsWith("\"") && expr.endsWith("\"")) { + return resolveType("String"); + } + + // Character literals + if (expr.startsWith("'") && expr.endsWith("'")) { + return TypeInfo.forPrimitive("char"); + } + + // Boolean literals + if (expr.equals("true") || expr.equals("false")) { + return TypeInfo.forPrimitive("boolean"); + } + + // Null literal + if (expr.equals("null")) { + return null; // null is compatible with any reference type + } + + // Numeric literals + if (expr.matches("-?\\d+[lL]?")) { + if (expr.toLowerCase().endsWith("l")) { + return TypeInfo.forPrimitive("long"); + } + return TypeInfo.forPrimitive("int"); + } + if (expr.matches("-?\\d+\\.\\d*[fF]?")) { + if (expr.toLowerCase().endsWith("f")) { + return TypeInfo.forPrimitive("float"); + } + return TypeInfo.forPrimitive("double"); + } + if (expr.matches("-?\\d+\\.\\d*[dD]")) { + return TypeInfo.forPrimitive("double"); + } + + // Simple variable reference + if (expr.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { + FieldInfo varInfo = resolveVariable(expr, position); + if (varInfo != null) { + return varInfo.getTypeInfo(); + } + } + + // "this" keyword + if (expr.equals("this")) { + // Could return the enclosing class type if we had that info + return null; + } + + // "new Type()" expressions + if (expr.startsWith("new ")) { + Matcher newMatcher = NEW_TYPE_PATTERN.matcher(expr); + if (newMatcher.find()) { + return resolveType(newMatcher.group(1)); + } + } + + // Chain expressions like "obj.field" or "obj.method()" + // This is complex - for now, return null to indicate unknown + // The type compatibility check will be lenient with null types + + return null; } /** @@ -940,6 +1616,9 @@ public static int foo(int x, int y, Boolean z) { * For example, for "mc.thePlayer.worldObj.weatherEffects.get()", * this would resolve mc -> Minecraft -> thePlayer -> EntityPlayer -> worldObj -> World -> weatherEffects -> List * and return the final TypeInfo (List). + * + * Also handles method calls in chains like "Minecraft.getMinecraft().thePlayer" by resolving + * the return type of getMinecraft() and continuing resolution. * * @param methodNameStart The start position of the method name * @return The TypeInfo of the final receiver, or null if no receiver or couldn't resolve @@ -954,9 +1633,9 @@ private TypeInfo resolveReceiverChain(int methodNameStart) { return null; // No receiver } - // Walk backward to collect all identifiers in the chain - List chainSegments = new ArrayList<>(); - List segmentPositions = new ArrayList<>(); + // Walk backward to collect all segments in the chain + // Segments can be identifiers OR method calls (identifier followed by parentheses) + List chainSegments = new ArrayList<>(); int pos = scanPos; // Currently at the dot @@ -971,28 +1650,50 @@ private TypeInfo resolveReceiverChain(int methodNameStart) { if (pos < 0) break; - // Check if this is the end of an identifier - if (!Character.isJavaIdentifierPart(text.charAt(pos))) { - // Could be ')' from a method call - we need to skip that - if (text.charAt(pos) == ')') { - // This is a method call result, e.g., getList().get() - // For now, we can't resolve method return types in chains - // So we return null to indicate unresolvable - return null; + char c = text.charAt(pos); + + // Check if this is the end of a method call (closing paren) + if (c == ')') { + // Find the matching opening paren + int closeParen = pos; + int openParen = findMatchingParenBackward(pos); + if (openParen < 0) { + return null; // Malformed + } + + // Now find the method name before the open paren + int beforeParen = openParen - 1; + while (beforeParen >= 0 && Character.isWhitespace(text.charAt(beforeParen))) + beforeParen--; + + if (beforeParen < 0 || !Character.isJavaIdentifierPart(text.charAt(beforeParen))) { + return null; // No method name } + + // Read the method name backward + int methodNameEnd = beforeParen + 1; + while (beforeParen >= 0 && Character.isJavaIdentifierPart(text.charAt(beforeParen))) + beforeParen--; + int methodNameBegin = beforeParen + 1; + + String methodName = text.substring(methodNameBegin, methodNameEnd); + chainSegments.add(0, new ChainSegment(methodName, methodNameBegin, methodNameEnd, true)); + + pos = beforeParen; + } else if (Character.isJavaIdentifierPart(c)) { + // Regular identifier (field or variable) + int identEnd = pos + 1; + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) + pos--; + int identStart = pos + 1; + + String ident = text.substring(identStart, identEnd); + chainSegments.add(0, new ChainSegment(ident, identStart, identEnd, false)); + } else { + // Unexpected character - end of chain break; } - // Read the identifier backward - int identEnd = pos + 1; - while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) - pos--; - int identStart = pos + 1; - - String ident = text.substring(identStart, identEnd); - chainSegments.add(0, ident); // Add to front (we're going backward) - segmentPositions.add(0, new int[]{identStart, identEnd}); - // Skip whitespace while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--; @@ -1012,64 +1713,172 @@ private TypeInfo resolveReceiverChain(int methodNameStart) { } // Now resolve the chain from left to right - String firstSegment = chainSegments.get(0); + ChainSegment firstSeg = chainSegments.get(0); + String firstSegment = firstSeg.name; TypeInfo currentType = null; if (firstSegment.equals("this")) { - // For 'this', we use globalFields for resolution - // Can't fully resolve 'this' without class context, but we can try - currentType = null; - - if (chainSegments.size() > 1) { - // this.something - check globalFields - String nextField = chainSegments.get(1); - if (globalFields.containsKey(nextField)) { - currentType = globalFields.get(nextField).getTypeInfo(); + // For 'this', check if we're inside a script-defined class + currentType = findEnclosingScriptType(methodNameStart); + + if (currentType == null && chainSegments.size() > 1) { + // Fallback: this.something - check globalFields + ChainSegment nextSeg = chainSegments.get(1); + if (!nextSeg.isMethodCall && globalFields.containsKey(nextSeg.name)) { + currentType = globalFields.get(nextSeg.name).getTypeInfo(); // Continue resolving from index 2 for (int i = 2; i < chainSegments.size(); i++) { - if (currentType == null || !currentType.isResolved()) - return null; - String field = chainSegments.get(i); - if (currentType.hasField(field)) { - FieldInfo fieldInfo = currentType.getFieldInfo(field); - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; - } else { - return null; - } + currentType = resolveChainSegment(currentType, chainSegments.get(i)); + if (currentType == null) return null; } return currentType; } } + // If we have a script type, continue from index 1 + if (currentType != null) { + for (int i = 1; i < chainSegments.size(); i++) { + currentType = resolveChainSegment(currentType, chainSegments.get(i)); + if (currentType == null) return null; + } + return currentType; + } return null; } else if (Character.isUpperCase(firstSegment.charAt(0))) { // Static access like Minecraft.getMinecraft() - the type is the class itself currentType = resolveType(firstSegment); - } else { + } else if (!firstSeg.isMethodCall) { // Variable like mc.thePlayer.worldObj - int[] firstPos = segmentPositions.get(0); - FieldInfo varInfo = resolveVariable(firstSegment, firstPos[0]); + FieldInfo varInfo = resolveVariable(firstSegment, firstSeg.start); if (varInfo != null) { currentType = varInfo.getTypeInfo(); } + } else { + // First segment is a method call without a receiver - check script methods + if (isScriptMethod(firstSegment)) { + MethodInfo scriptMethod = getScriptMethodInfo(firstSegment); + if (scriptMethod != null) { + currentType = scriptMethod.getReturnType(); + } + } } // Resolve remaining segments for (int i = 1; i < chainSegments.size(); i++) { - if (currentType == null || !currentType.isResolved()) { - return null; + currentType = resolveChainSegment(currentType, chainSegments.get(i)); + if (currentType == null) return null; + } + + return currentType; + } + + /** + * Helper class for chain segments (can be field access or method call). + */ + private static class ChainSegment { + final String name; + final int start; + final int end; + final boolean isMethodCall; + + ChainSegment(String name, int start, int end, boolean isMethodCall) { + this.name = name; + this.start = start; + this.end = end; + this.isMethodCall = isMethodCall; + } + } + + /** + * Resolve a single segment of a chain given the current type context. + */ + private TypeInfo resolveChainSegment(TypeInfo currentType, ChainSegment segment) { + if (currentType == null || !currentType.isResolved()) { + return null; + } + + if (segment.isMethodCall) { + // Method call - get return type + if (currentType.hasMethod(segment.name)) { + MethodInfo methodInfo = currentType.getMethodInfo(segment.name); + return (methodInfo != null) ? methodInfo.getReturnType() : null; } + return null; + } else { + // Field access + if (currentType.hasField(segment.name)) { + FieldInfo fieldInfo = currentType.getFieldInfo(segment.name); + return (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } + return null; + } + } - String field = chainSegments.get(i); - if (currentType.hasField(field)) { - FieldInfo fieldInfo = currentType.getFieldInfo(field); - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; - } else { - // Field doesn't exist - return null; + /** + * Find the matching opening parenthesis when given a closing paren position. + */ + private int findMatchingParenBackward(int closeParenPos) { + if (closeParenPos < 0 || closeParenPos >= text.length() || text.charAt(closeParenPos) != ')') { + return -1; + } + + int depth = 1; + boolean inString = false; + char stringChar = 0; + + for (int i = closeParenPos - 1; i >= 0; i--) { + char c = text.charAt(i); + char next = (i < text.length() - 1) ? text.charAt(i + 1) : 0; + + // Handle string literals (going backward, check for escapes) + if (!inString && (c == '"' || c == '\'')) { + // Check if this quote is escaped (look back for backslash) + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) { + backslashCount++; + } + if (backslashCount % 2 == 0) { + inString = true; + stringChar = c; + } + } else if (inString && c == stringChar) { + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) { + backslashCount++; + } + if (backslashCount % 2 == 0) { + inString = false; + } + } + + if (inString) continue; + + // Skip excluded regions (comments) + if (isExcluded(i)) continue; + + if (c == ')') { + depth++; + } else if (c == '(') { + depth--; + if (depth == 0) { + return i; + } } } + + return -1; // No matching paren found + } - return currentType; + /** + * Find the script-defined type that contains the given position. + * Used for resolving 'this' references. + */ + private ScriptTypeInfo findEnclosingScriptType(int position) { + for (ScriptTypeInfo type : scriptTypes.values()) { + if (type.containsPosition(position)) { + return type; + } + } + return null; } /** @@ -1084,6 +1893,18 @@ private boolean isScriptMethod(String methodName) { return false; } + /** + * Get the MethodInfo for a script-defined method by name. + */ + private MethodInfo getScriptMethodInfo(String methodName) { + for (MethodInfo method : methods) { + if (method.getName().equals(methodName)) { + return method; + } + } + return null; + } + private void markVariables(List marks) { Set knownKeywords = new HashSet<>(Arrays.asList( "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 819c27965..f2268e242 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -1,6 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter; +import net.minecraft.client.gui.Gui; import noppes.npcs.client.ClientProxy; +import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.Collections; @@ -146,6 +148,13 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume token.setFieldInfo((FieldInfo) mark.metadata); } else if (mark.metadata instanceof MethodInfo) { token.setMethodInfo((MethodInfo) mark.metadata); + } else if (mark.metadata instanceof MethodCallInfo) { + MethodCallInfo callInfo = (MethodCallInfo) mark.metadata; + token.setMethodCallInfo(callInfo); + if (callInfo.getErrorType() != MethodCallInfo.ErrorType.NONE) { + // For other errors (arg count, static access), underline the method name + token.setUnderline(true, 0xFF5555); // Red wavy underline + } } else if (mark.metadata instanceof ImportData) { token.setImportData((ImportData) mark.metadata); } @@ -190,19 +199,41 @@ public void addIndentGuide(int column) { /** * Draw this line with syntax highlighting using Minecraft color codes. * Compatible with the existing rendering system. + * Also draws curly underlines for tokens with errors (method call validation failures). */ public void drawString(int x, int y, int defaultColor) { StringBuilder builder = new StringBuilder(); int lastIndex = 0; + // Track positions for underlines + List underlines = new ArrayList<>(); // [startX, endX, color] + int currentX = x; + for (Token t : tokens) { int tokenStart = t.getGlobalStart() - globalStart; // relative position in line - - // Append any text before this token (spaces, punctuation, etc.) + + // Calculate gap width before this token if (tokenStart > lastIndex && tokenStart <= text.length()) { - builder.append(text, lastIndex, tokenStart); + String gapText = text.substring(lastIndex, tokenStart); + builder.append(gapText); + currentX += ClientProxy.Font.width(gapText); + } + + // Track underline position if token has one + if (t.hasUnderline()) { + int tokenWidth = ClientProxy.Font.width(t.getText()); + underlines.add(new int[]{currentX, currentX + tokenWidth, t.getUnderlineColor()}); + } + + if (t.getMethodCallInfo() != null) { + MethodCallInfo callInfo = t.getMethodCallInfo(); + if (callInfo.hasArgTypeError(t)) { + underlines.add( + new int[]{currentX, currentX + ClientProxy.Font.width(t.getText()), t.getUnderlineColor()}); + } } + // Append the colored token builder.append(COLOR_CHAR) .append(t.getColorCode()) @@ -210,6 +241,7 @@ public void drawString(int x, int y, int defaultColor) { .append(COLOR_CHAR) .append('f'); // Reset to white + currentX += ClientProxy.Font.width(t.getText()); lastIndex = tokenStart + t.getText().length(); } @@ -218,16 +250,67 @@ public void drawString(int x, int y, int defaultColor) { builder.append(text.substring(lastIndex)); } + // Draw the text ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); + + // Draw curly underlines for error tokens + for (int[] ul : underlines) { + drawCurlyUnderline(ul[0], y + ClientProxy.Font.height() - 1, ul[1] - ul[0], ul[2]); + } + } + + /** + * Draw a curly/wavy underline (like IDE error highlighting). + * @param x Start X position + * @param y Y position (bottom of text) + * @param width Width of the underline + * @param color Color in ARGB format (e.g., 0xFFFF5555 for red) + */ + private void drawCurlyUnderline(int x, int y, int width, int color) { + if (width <= 0) + return; + + float a = ((color >> 24) & 0xFF) / 255f; + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = (color & 0xFF) / 255f; + + // If alpha is 0, assume full opacity + if (a == 0) + a = 1.0f; + + GL11.glPushMatrix(); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + GL11.glLineWidth(1.0f); + + GL11.glBegin(GL11.GL_LINE_STRIP); + // Wave parameters: 2 pixels amplitude, 4 pixels wavelength + int waveHeight = 2; + int waveLength = 4; + for (int i = 0; i <= width; i++) { + // Create a sine-like wave pattern + double phase = (double) i / waveLength * Math.PI * 2; + int yOffset = (int) (Math.sin(phase) * waveHeight); + GL11.glVertex2f(x + i, y + yOffset); + } + GL11.glEnd(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); } /** * Draw this line with hex colors instead of Minecraft color codes. * More flexible but requires custom font rendering. + * Also draws wavy underlines for tokens with errors. * * @param x X position * @param y Y position - * @param renderer A functional interface for drawing colored text segments + * @param renderer A renderer for drawing colored text and error underlines */ public void drawStringHex(int x, int y, HexColorRenderer renderer) { int currentX = x; @@ -239,31 +322,85 @@ public void drawStringHex(int x, int y, HexColorRenderer renderer) { // Draw any text before this token in default color if (tokenStart > lastIndex && tokenStart <= text.length()) { String gap = text.substring(lastIndex, tokenStart); - currentX = renderer.draw(gap, currentX, y, 0xFFFFFF); + currentX = renderer.draw(gap, currentX, y, 0xFFFFFF, false, 0); } - // Draw the colored token - currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor()); + // Draw the colored token (with underline if flagged) + currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor(), t.hasUnderline(), + t.getUnderlineColor()); lastIndex = tokenStart + t.getText().length(); } // Draw any remaining text in default color if (lastIndex < text.length()) { - renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); + renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF, false, 0); } } /** - * Functional interface for rendering text with hex colors. + * Functional interface for rendering text with hex colors and optional underlines. */ @FunctionalInterface public interface HexColorRenderer { /** * Draw text at the specified position with the given color. + * @param text The text to draw + * @param x X position + * @param y Y position + * @param hexColor The text color in hex format + * @param underline Whether to draw an underline (wavy for errors) + * @param underlineColor The underline color (if underline is true) * @return The X position after drawing (for continuation) */ - int draw(String text, int x, int y, int hexColor); + int draw(String text, int x, int y, int hexColor, boolean underline, int underlineColor); + } + + /** + * Creates a default HexColorRenderer implementation using ClientProxy.Font. + * This renderer draws text with hex colors and curly underlines for errors. + * @return A HexColorRenderer that can be passed to drawStringHex + */ + public static HexColorRenderer createDefaultHexRenderer() { + return (text, x, y, hexColor, underline, underlineColor) -> { + // Draw the text with hex color + // Minecraft's font renderer uses ARGB, so add full alpha if not present + int color = (hexColor & 0xFF000000) == 0 ? (0xFF000000 | hexColor) : hexColor; + ClientProxy.Font.drawString(text, x, y, color); + int textWidth = ClientProxy.Font.width(text); + + // Draw curly underline if needed + if (underline && textWidth > 0) { + int ulColor = (underlineColor & 0xFF000000) == 0 ? (0xFF000000 | underlineColor) : underlineColor; + + float a = ((ulColor >> 24) & 0xFF) / 255f; + float r = ((ulColor >> 16) & 0xFF) / 255f; + float g = ((ulColor >> 8) & 0xFF) / 255f; + float b = (ulColor & 0xFF) / 255f; + + GL11.glPushMatrix(); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + GL11.glLineWidth(1.0f); + + int underlineY = y + ClientProxy.Font.height() - 1; + GL11.glBegin(GL11.GL_LINE_STRIP); + for (int i = 0; i <= textWidth; i++) { + double phase = (double) i / 4 * Math.PI * 2; + int yOffset = (int) (Math.sin(phase) * 2); + GL11.glVertex2f(x + i, underlineY + yOffset); + } + GL11.glEnd(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + } + + return x + textWidth; + }; } // ==================== UTILITIES ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java new file mode 100644 index 000000000..b0ab251a8 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java @@ -0,0 +1,187 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a type defined within the script itself (not from Java classpath). + * This handles classes, interfaces, and enums declared in the user's script. + * + * Unlike TypeInfo which wraps a Java Class, ScriptTypeInfo stores all metadata + * about fields, methods, and other type information parsed from the script. + */ +public class ScriptTypeInfo extends TypeInfo { + + private final String scriptClassName; + private final int declarationOffset; + private final int bodyStart; + private final int bodyEnd; + + // Script-defined members + private final Map fields = new HashMap<>(); + private final Map> methods = new HashMap<>(); // name -> list of overloads + private final List innerClasses = new ArrayList<>(); + + // Parent class reference (for inner class resolution) + private ScriptTypeInfo outerClass; + + private ScriptTypeInfo(String simpleName, String fullName, Kind kind, + int declarationOffset, int bodyStart, int bodyEnd) { + super(simpleName, fullName, "", kind, null, true, null, true); + this.scriptClassName = simpleName; + this.declarationOffset = declarationOffset; + this.bodyStart = bodyStart; + this.bodyEnd = bodyEnd; + } + + // Factory method + public static ScriptTypeInfo create(String simpleName, Kind kind, + int declarationOffset, int bodyStart, int bodyEnd) { + return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd); + } + + public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTypeInfo outer, + int declarationOffset, int bodyStart, int bodyEnd) { + String fullName = outer.getFullName() + "$" + simpleName; + ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd); + inner.outerClass = outer; + outer.innerClasses.add(inner); + return inner; + } + + // Getters + public String getScriptClassName() { return scriptClassName; } + public int getDeclarationOffset() { return declarationOffset; } + public int getBodyStart() { return bodyStart; } + public int getBodyEnd() { return bodyEnd; } + public ScriptTypeInfo getOuterClass() { return outerClass; } + public List getInnerClasses() { return innerClasses; } + + /** + * Check if a position is inside this type's body. + */ + public boolean containsPosition(int position) { + return position >= bodyStart && position < bodyEnd; + } + + // ==================== FIELD MANAGEMENT ==================== + + public void addField(FieldInfo field) { + fields.put(field.getName(), field); + } + + @Override + public boolean hasField(String fieldName) { + return fields.containsKey(fieldName); + } + + @Override + public FieldInfo getFieldInfo(String fieldName) { + return fields.get(fieldName); + } + + public Map getFields() { + return new HashMap<>(fields); + } + + // ==================== METHOD MANAGEMENT ==================== + + public void addMethod(MethodInfo method) { + methods.computeIfAbsent(method.getName(), k -> new ArrayList<>()).add(method); + } + + @Override + public boolean hasMethod(String methodName) { + return methods.containsKey(methodName); + } + + @Override + public boolean hasMethod(String methodName, int paramCount) { + List overloads = methods.get(methodName); + if (overloads == null) return false; + for (MethodInfo m : overloads) { + if (m.getParameterCount() == paramCount) { + return true; + } + } + return false; + } + + @Override + public MethodInfo getMethodInfo(String methodName) { + List overloads = methods.get(methodName); + return (overloads != null && !overloads.isEmpty()) ? overloads.get(0) : null; + } + + /** + * Get all method overloads with the given name. + */ + public List getMethodOverloads(String methodName) { + return methods.getOrDefault(methodName, new ArrayList<>()); + } + + /** + * Get a method with specific parameter count. + */ + public MethodInfo getMethodWithParamCount(String methodName, int paramCount) { + List overloads = methods.get(methodName); + if (overloads == null) return null; + for (MethodInfo m : overloads) { + if (m.getParameterCount() == paramCount) { + return m; + } + } + return null; + } + + public Map> getMethods() { + return new HashMap<>(methods); + } + + // ==================== INNER CLASS LOOKUP ==================== + + /** + * Find an inner class by name. + */ + public ScriptTypeInfo getInnerClass(String name) { + for (ScriptTypeInfo inner : innerClasses) { + if (inner.getSimpleName().equals(name)) { + return inner; + } + } + return null; + } + + @Override + public TokenType getTokenType() { + switch (getKind()) { + case INTERFACE: + return TokenType.INTERFACE_DECL; + case ENUM: + return TokenType.ENUM_DECL; + case CLASS: + default: + return TokenType.CLASS_DECL; + } + } + + // Script types are always considered resolved since they're defined in the script + @Override + public boolean isResolved() { + return true; + } + + // Script types don't have a Java class + @Override + public Class getJavaClass() { + return null; + } + + @Override + public String toString() { + return "ScriptTypeInfo{" + scriptClassName + ", " + getKind() + + ", fields=" + fields.size() + ", methods=" + methods.size() + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index cbd8cfc4f..de8ed2a66 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -21,7 +21,12 @@ public class Token { private TypeInfo typeInfo; // For class references, type declarations private FieldInfo fieldInfo; // For field references private MethodInfo methodInfo; // For method calls/declarations + private MethodCallInfo methodCallInfo; // For method calls with argument info private ImportData importData; // For import statements + + // Rendering flags + private boolean hasUnderline; // True if this token should be underlined (for errors) + private int underlineColor; // Color of the underline (if any) // Navigation - set by ScriptLine private Token prev; @@ -111,8 +116,11 @@ public static Token undefined(String text, int start, int end) { public TypeInfo getTypeInfo() { return typeInfo; } public FieldInfo getFieldInfo() { return fieldInfo; } public MethodInfo getMethodInfo() { return methodInfo; } + public MethodCallInfo getMethodCallInfo() { return methodCallInfo; } public ImportData getImportData() { return importData; } public ScriptLine getParentLine() { return parentLine; } + public boolean hasUnderline() { return hasUnderline; } + public int getUnderlineColor() { return underlineColor; } // ==================== SETTERS ==================== @@ -120,7 +128,13 @@ public static Token undefined(String text, int start, int end) { public void setTypeInfo(TypeInfo info) { this.typeInfo = info; } public void setFieldInfo(FieldInfo info) { this.fieldInfo = info; } public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } + public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } + + public void setUnderline(boolean hasUnderline, int color) { + this.hasUnderline = hasUnderline; + this.underlineColor = color; + } void setParentLine(ScriptLine line) { this.parentLine = line; } void setPrev(Token prev) { this.prev = prev; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java index 50a74679b..e5a0b0bdc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -23,10 +23,12 @@ public enum TokenType { CLASS_DECL(0x00AAAA, 85), // class names in declarations IMPORTED_CLASS(0x00AAAA, 75), // imported class usages TYPE_DECL(0x00AAAA, 70), // package paths, type references + UNUSED_IMPORT(0x666666, 65), // unused import statements (gray) // Methods METHOD_DECL(0x00AA00, 60), // method declarations (green) METHOD_CALL(0x55FF55, 50), // method calls (bright green) + METHOD_CALL_ERROR(0xFF5555, 51), // method call with validation error (red underline) // Variables and fields UNDEFINED_VAR(0xAA0000, 20), // unresolved variables (dark red) - high priority @@ -65,11 +67,13 @@ public char toColorCode() { switch (this) { case COMMENT: case NUMBER: + case UNUSED_IMPORT: return '7'; // gray case STRING: return '5'; // purple case CLASS_KEYWORD: case KEYWORD: + case METHOD_CALL_ERROR: return 'c'; // red case IMPORT_KEYWORD: case MODIFIER: diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java index e2b0099af..3967ed535 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -2,9 +2,9 @@ /** * Represents resolved type information for a class/interface/enum. - * Immutable data class holding all type metadata. + * Base class for type metadata. Extended by ScriptTypeInfo for script-defined types. */ -public final class TypeInfo { +public class TypeInfo { public enum Kind { CLASS, @@ -31,6 +31,19 @@ private TypeInfo(String simpleName, String fullName, String packageName, this.resolved = resolved; this.enclosingType = enclosingType; } + + // Protected constructor for subclasses (like ScriptTypeInfo) + protected TypeInfo(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, + @SuppressWarnings("unused") boolean subclass) { + this.simpleName = simpleName; + this.fullName = fullName; + this.packageName = packageName; + this.kind = kind; + this.javaClass = javaClass; + this.resolved = resolved; + this.enclosingType = enclosingType; + } // Factory methods public static TypeInfo resolved(String simpleName, String fullName, String packageName, @@ -74,6 +87,25 @@ public static TypeInfo fromClass(Class clazz) { return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing); } + /** + * Create a TypeInfo for a primitive type. + */ + public static TypeInfo forPrimitive(String typeName) { + Class primitiveClass = null; + switch (typeName) { + case "boolean": primitiveClass = boolean.class; break; + case "byte": primitiveClass = byte.class; break; + case "char": primitiveClass = char.class; break; + case "short": primitiveClass = short.class; break; + case "int": primitiveClass = int.class; break; + case "long": primitiveClass = long.class; break; + case "float": primitiveClass = float.class; break; + case "double": primitiveClass = double.class; break; + case "void": primitiveClass = void.class; break; + } + return new TypeInfo(typeName, typeName, "", Kind.CLASS, primitiveClass, true, null); + } + // Getters public String getSimpleName() { return simpleName; } public String getFullName() { return fullName; } From d9f16fef8a905107bbe4b2ab015d79e16b721b19 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 20:10:10 +0200 Subject: [PATCH 007/337] Stored MethodCallInfo and FieldCallInfo properly for method call args, and underlined them when errored for 2 error types; wrong method call args count and wrong arg type --- .../util/script/interpreter/FieldInfo.java | 14 +++++ .../script/interpreter/MethodCallInfo.java | 5 ++ .../script/interpreter/ScriptDocument.java | 62 ++++++++++++------- .../util/script/interpreter/ScriptLine.java | 27 +++----- .../gui/util/script/interpreter/Token.java | 19 +++++- 5 files changed, 83 insertions(+), 44 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java index dc8f0da89..6fd9ff0e1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -125,4 +125,18 @@ public boolean equals(Object o) { public int hashCode() { return name.hashCode() * 31 + scope.ordinal(); } + + /** + * Wrapper class that holds both FieldInfo and MethodCallInfo for a variable usage. + * This is used when a variable is used as an argument to a method call. + */ + public static class ArgInfo { + public final FieldInfo fieldInfo; + public final MethodCallInfo methodCallInfo; + + public ArgInfo(FieldInfo fieldInfo, MethodCallInfo methodCallInfo) { + this.fieldInfo = fieldInfo; + this.methodCallInfo = methodCallInfo; + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java index 79b7198a5..a4b16eba1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -202,6 +202,7 @@ public boolean hasArgCountError() { public boolean hasArgTypeError() { return !this.argumentTypeErrors.isEmpty(); } + public boolean hasArgTypeError(Token t) { for (ArgumentTypeError ate : this.argumentTypeErrors) { if (ate.getArg().equals(t)) { @@ -211,6 +212,10 @@ public boolean hasArgTypeError(Token t) { return false; } + public boolean hasArgCountError(Token t) { + return hasArgCountError() && t.isMethodCall(); + } + /** * Check if this is a static access error (underline method name). */ 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 e5ecc117a..83a9bff89 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 @@ -1,6 +1,5 @@ package noppes.npcs.client.gui.util.script.interpreter; -import net.minecraft.client.Minecraft; import noppes.npcs.client.ClientProxy; import java.util.*; @@ -74,6 +73,9 @@ public class ScriptDocument { // Local variables per method (methodStartOffset -> {varName -> FieldInfo}) private final Map> methodLocals = new HashMap<>(); + // Method calls - stores all parsed method call information + private final List methodCalls = new ArrayList<>(); + // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); @@ -180,6 +182,7 @@ public void formatCodeText() { excludedRanges.clear(); methodLocals.clear(); scriptTypes.clear(); + methodCalls.clear(); // Phase 1: Find excluded regions (strings/comments) findExcludedRanges(); @@ -858,13 +861,14 @@ private List buildMarks() { // Numbers addPatternMarks(marks, NUMBER_PATTERN, TokenType.NUMBER); + // Method calls (parse before variables so we can attach context) + markMethodCalls(marks); + // Variables and fields markVariables(marks); // Chained field accesses (e.g., mc.player.world, this.field) markChainedFieldAccesses(marks); - - markMethodCalls(marks); // Imported class usages markImportedClassUsages(marks); @@ -1321,6 +1325,9 @@ private void markMethodCalls(List marks) { arguments, receiverType, resolvedMethod, isStaticAccess ); callInfo.validate(); + + // Store in collection for later lookup + methodCalls.add(callInfo); // Always mark method call as green (METHOD_CALL), with error info attached for underline // The MethodCallInfo is attached so rendering can draw curly underline if there's an error @@ -1349,6 +1356,9 @@ private void markMethodCalls(List marks) { arguments, null, resolvedMethod ); callInfo.validate(); + + // Store in collection for later lookup + methodCalls.add(callInfo); // Always mark method call as green (METHOD_CALL), with error info attached for underline marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); @@ -1422,6 +1432,25 @@ private boolean isStaticAccessCall(int methodNameStart) { return !ident.isEmpty() && Character.isUpperCase(ident.charAt(0)); } + /** + * Find the MethodCallInfo that contains the given position as an argument. + * Returns null if the position is not inside any method call's argument list. + */ + private MethodCallInfo findMethodCallContainingPosition(int position) { + for (MethodCallInfo call : methodCalls) { + // Check if position is within the argument list (between open and close parens) + if (position >= call.getOpenParenOffset() && position <= call.getCloseParenOffset()) { + // Check if it's within any of the arguments + for (MethodCallInfo.Argument arg : call.getArguments()) { + if (position >= arg.getStartOffset() && position <= arg.getEndOffset()) { + return call; + } + } + } + } + return null; + } + /** * Find the matching closing parenthesis for an opening parenthesis. * Handles nested parentheses, strings, and comments. @@ -1951,6 +1980,8 @@ private void markVariables(List marks) { // Find containing method MethodInfo containingMethod = findMethodAtPosition(position); + // Check if this variable is an argument to a method call + MethodCallInfo callInfo = findMethodCallContainingPosition(position); // For uppercase identifiers, only process if it's a known field // Otherwise, let type handling (markImportedClassUsages) handle it @@ -1970,30 +2001,17 @@ private void markVariables(List marks) { FieldInfo localInfo = locals.get(name); // Only highlight if the position is after the declaration if (localInfo.isVisibleAt(position)) { - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, localInfo)); + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(localInfo, callInfo) : localInfo; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, metadata)); continue; } } - - // Check global fields - if (globalFields.containsKey(name)) { - FieldInfo fieldInfo = globalFields.get(name); - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, fieldInfo)); - continue; - } - - // Skip uppercase if not a known field - type handling will deal with it - if (isUppercase) - continue; - - // Unknown variable inside method - mark as undefined - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); } else { - // Outside any method // Check global fields if (globalFields.containsKey(name)) { FieldInfo fieldInfo = globalFields.get(name); - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, fieldInfo)); + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); continue; } @@ -2001,8 +2019,8 @@ private void markVariables(List marks) { if (isUppercase) continue; - // Unknown variable outside method - mark as undefined - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + // Unknown variable inside method - mark as undefined + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR, callInfo)); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index f2268e242..a95d0a6b1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -1,6 +1,5 @@ package noppes.npcs.client.gui.util.script.interpreter; -import net.minecraft.client.gui.Gui; import noppes.npcs.client.ClientProxy; import org.lwjgl.opengl.GL11; @@ -144,6 +143,10 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume if (mark.metadata != null) { if (mark.metadata instanceof TypeInfo) { token.setTypeInfo((TypeInfo) mark.metadata); + } else if (mark.metadata instanceof FieldInfo.ArgInfo) { + FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) mark.metadata; + token.setFieldInfo(ctx.fieldInfo); + token.setMethodCallInfo(ctx.methodCallInfo); } else if (mark.metadata instanceof FieldInfo) { token.setFieldInfo((FieldInfo) mark.metadata); } else if (mark.metadata instanceof MethodInfo) { @@ -206,7 +209,6 @@ public void drawString(int x, int y, int defaultColor) { int lastIndex = 0; // Track positions for underlines - List underlines = new ArrayList<>(); // [startX, endX, color] int currentX = x; for (Token t : tokens) { @@ -222,18 +224,10 @@ public void drawString(int x, int y, int defaultColor) { // Track underline position if token has one if (t.hasUnderline()) { int tokenWidth = ClientProxy.Font.width(t.getText()); - underlines.add(new int[]{currentX, currentX + tokenWidth, t.getUnderlineColor()}); + int currentY = y + ClientProxy.Font.height() - 1; + drawCurlyUnderline(currentX + 1, currentY, tokenWidth + 1, t.getUnderlineColor()); } - if (t.getMethodCallInfo() != null) { - MethodCallInfo callInfo = t.getMethodCallInfo(); - if (callInfo.hasArgTypeError(t)) { - underlines.add( - new int[]{currentX, currentX + ClientProxy.Font.width(t.getText()), t.getUnderlineColor()}); - } - } - - // Append the colored token builder.append(COLOR_CHAR) .append(t.getColorCode()) @@ -252,11 +246,6 @@ public void drawString(int x, int y, int defaultColor) { // Draw the text ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); - - // Draw curly underlines for error tokens - for (int[] ul : underlines) { - drawCurlyUnderline(ul[0], y + ClientProxy.Font.height() - 1, ul[1] - ul[0], ul[2]); - } } /** @@ -288,12 +277,12 @@ private void drawCurlyUnderline(int x, int y, int width, int color) { GL11.glBegin(GL11.GL_LINE_STRIP); // Wave parameters: 2 pixels amplitude, 4 pixels wavelength - int waveHeight = 2; + int waveHeight = 1; int waveLength = 4; for (int i = 0; i <= width; i++) { // Create a sine-like wave pattern double phase = (double) i / waveLength * Math.PI * 2; - int yOffset = (int) (Math.sin(phase) * waveHeight); + float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.5f; GL11.glVertex2f(x + i, y + yOffset); } GL11.glEnd(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index de8ed2a66..7c6851d69 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -26,7 +26,7 @@ public class Token { // Rendering flags private boolean hasUnderline; // True if this token should be underlined (for errors) - private int underlineColor; // Color of the underline (if any) + private int underlineColor = 0xFF5555; // Color of the underline (if any) // Navigation - set by ScriptLine private Token prev; @@ -119,8 +119,21 @@ public static Token undefined(String text, int start, int end) { public MethodCallInfo getMethodCallInfo() { return methodCallInfo; } public ImportData getImportData() { return importData; } public ScriptLine getParentLine() { return parentLine; } - public boolean hasUnderline() { return hasUnderline; } - public int getUnderlineColor() { return underlineColor; } + + public boolean hasUnderline() { + if (methodCallInfo != null) { + if (methodCallInfo.hasArgCountError(this)) + return true; + else if (methodCallInfo.hasArgTypeError(this)) { + return true; + } + } + return false; + } + + public int getUnderlineColor() { + return underlineColor; + } // ==================== SETTERS ==================== From 65953d77c4a51fc427dbd35f65323abc4c50a328 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 20:19:01 +0200 Subject: [PATCH 008/337] Removed unused --- .../npcs/client/gui/util/script/interpreter/TokenType.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java index e5a0b0bdc..90ab87725 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -28,7 +28,6 @@ public enum TokenType { // Methods METHOD_DECL(0x00AA00, 60), // method declarations (green) METHOD_CALL(0x55FF55, 50), // method calls (bright green) - METHOD_CALL_ERROR(0xFF5555, 51), // method call with validation error (red underline) // Variables and fields UNDEFINED_VAR(0xAA0000, 20), // unresolved variables (dark red) - high priority @@ -73,7 +72,6 @@ public char toColorCode() { return '5'; // purple case CLASS_KEYWORD: case KEYWORD: - case METHOD_CALL_ERROR: return 'c'; // red case IMPORT_KEYWORD: case MODIFIER: From f6539b071ce2e1dce106cd2dd6928232bb3ed3a3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 20:28:47 +0200 Subject: [PATCH 009/337] Highlighted unused imports --- .../npcs/client/gui/util/script/interpreter/TokenType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java index 90ab87725..24c8c059c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -9,6 +9,7 @@ public enum TokenType { // Comments and strings have highest priority - they override everything inside them COMMENT(0x777777, 130), STRING(0xCC8855, 120), + UNUSED_IMPORT(0x666666, 119), // unused import statements (gray) // Keywords and modifiers CLASS_KEYWORD(0xFF5555, 115), // 'class', 'interface', 'enum' keywords @@ -23,7 +24,6 @@ public enum TokenType { CLASS_DECL(0x00AAAA, 85), // class names in declarations IMPORTED_CLASS(0x00AAAA, 75), // imported class usages TYPE_DECL(0x00AAAA, 70), // package paths, type references - UNUSED_IMPORT(0x666666, 65), // unused import statements (gray) // Methods METHOD_DECL(0x00AA00, 60), // method declarations (green) From fbf60bafb4dcb196c12934a5e8c3275e36631094 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 21:02:08 +0200 Subject: [PATCH 010/337] Lil clean up --- .../npcs/client/gui/util/script/interpreter/ScriptLine.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index a95d0a6b1..72ddcb809 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -213,6 +213,7 @@ public void drawString(int x, int y, int defaultColor) { for (Token t : tokens) { int tokenStart = t.getGlobalStart() - globalStart; // relative position in line + int tokenWidth = ClientProxy.Font.width(t.getText()); // Calculate gap width before this token if (tokenStart > lastIndex && tokenStart <= text.length()) { @@ -223,7 +224,6 @@ public void drawString(int x, int y, int defaultColor) { // Track underline position if token has one if (t.hasUnderline()) { - int tokenWidth = ClientProxy.Font.width(t.getText()); int currentY = y + ClientProxy.Font.height() - 1; drawCurlyUnderline(currentX + 1, currentY, tokenWidth + 1, t.getUnderlineColor()); } @@ -235,7 +235,7 @@ public void drawString(int x, int y, int defaultColor) { .append(COLOR_CHAR) .append('f'); // Reset to white - currentX += ClientProxy.Font.width(t.getText()); + currentX += tokenWidth; lastIndex = tokenStart + t.getText().length(); } @@ -288,7 +288,6 @@ private void drawCurlyUnderline(int x, int y, int width, int color) { GL11.glEnd(); GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glDisable(GL11.GL_BLEND); GL11.glPopMatrix(); } From b9d1a9f65763f71e63fc31376234e6c8ba4912dd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 21:24:37 +0200 Subject: [PATCH 011/337] Chaining of fields/methods like "Minecraft.getMinecraft().thePlayer.worldObj" is now completely functional --- .../script/interpreter/ScriptDocument.java | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) 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 83a9bff89..baab017d4 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 @@ -2109,6 +2109,11 @@ private void markChainedFieldAccesses(List marks) { // Start with resolving the first segment TypeInfo currentType = null; boolean firstIsThis = firstSegment.equals("this"); + boolean firstIsPrecededByDot = isPrecededByDot(chainStart); + + // Determine the starting index for marking + // If the first segment is preceded by a dot, it's a field access (not a variable), so we should mark it + int startMarkingIndex = firstIsPrecededByDot ? 0 : 1; if (firstIsThis) { // For 'this', we don't have a class context to resolve, but we can mark subsequent fields @@ -2118,23 +2123,50 @@ private void markChainedFieldAccesses(List marks) { // Static field access like SomeClass.field currentType = resolveType(firstSegment); } else { - // Variable field access - FieldInfo varInfo = resolveVariable(firstSegment, chainStart); - if (varInfo != null) { - currentType = varInfo.getTypeInfo(); + // Variable or field access + if (firstIsPrecededByDot) { + // This is a field access on a previous result (e.g., getMinecraft().thePlayer) + // Resolve the receiver chain to get the type of what comes before the dot + TypeInfo receiverType = resolveReceiverChain(chainStart); + if (receiverType != null && receiverType.hasField(firstSegment)) { + FieldInfo varInfo = receiverType.getFieldInfo(firstSegment); + currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; + } else { + currentType = null; // Can't resolve + } + } else { + // This is a variable starting the chain + FieldInfo varInfo = resolveVariable(firstSegment, chainStart); + if (varInfo != null) { + currentType = varInfo.getTypeInfo(); + } else { + currentType = null; + } } } - // Mark the segments - skip first segment (already marked by markVariables or markImportedClassUsages) - for (int i = 1; i < chainSegments.size(); i++) { + // Mark the segments - start from index determined above + for (int i = startMarkingIndex; i < chainSegments.size(); i++) { String segment = chainSegments.get(i); int[] segPos = segmentPositions.get(i); if (isExcluded(segPos[0])) continue; - if (i == 1 && firstIsThis) { - // For "this.fiel/d", check if field exists in globalFields + // Handle the first segment if we're marking it (i.e., when preceded by dot) + if (i == 0 && firstIsPrecededByDot) { + // This is a field access on a receiver (e.g., the "thePlayer" in "getMinecraft().thePlayer") + TypeInfo receiverType = resolveReceiverChain(chainStart); + if (receiverType != null && receiverType.hasField(segment)) { + FieldInfo fieldInfo = receiverType.getFieldInfo(segment); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } else { + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); + currentType = null; + } + } else if (i == 1 && firstIsThis) { + // For "this.field", check if field exists in globalFields if (globalFields.containsKey(segment)) { FieldInfo fieldInfo = globalFields.get(segment); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); From 54cd31724d1c9190bfacccefbcb41bb3c78473a2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 22:55:20 +0200 Subject: [PATCH 012/337] Fixed thePlayer in "getMinecraft().thePlayer" not highlighting properly + more optimisations --- .../script/interpreter/ScriptDocument.java | 520 ++++++++++++------ 1 file changed, 342 insertions(+), 178 deletions(-) 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 baab017d4..6f9d63fdd 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 @@ -1310,60 +1310,39 @@ private void markMethodCalls(List marks) { // Check if this is a static access (Class.method() style) boolean isStaticAccess = isStaticAccessCall(nameStart); - // Resolve the receiver chain and get the final type + // Resolve receiver using existing chain-based resolver and detect static access TypeInfo receiverType = resolveReceiverChain(nameStart); MethodInfo resolvedMethod = null; + boolean computedStaticAccess = isStaticAccessCall(nameStart); if (receiverType != null) { - // We have a resolved receiver type - check if method exists if (receiverType.hasMethod(methodName)) { resolvedMethod = receiverType.getMethodInfo(methodName); - - // Create MethodCallInfo for validation (with static access flag) MethodCallInfo callInfo = new MethodCallInfo( methodName, nameStart, nameEnd, openParen, closeParen, - arguments, receiverType, resolvedMethod, isStaticAccess + arguments, receiverType, resolvedMethod, computedStaticAccess ); callInfo.validate(); - - // Store in collection for later lookup methodCalls.add(callInfo); - - // Always mark method call as green (METHOD_CALL), with error info attached for underline - // The MethodCallInfo is attached so rendering can draw curly underline if there's an error marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { - // Method doesn't exist on this type marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } } else { - // No receiver OR receiver couldn't be resolved - // Check if there's a dot before this (meaning there WAS a receiver, we just couldn't resolve it) boolean hasDot = isPrecededByDot(nameStart); - if (hasDot) { - // There was a receiver but we couldn't resolve it - mark as undefined marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } else { - // No receiver - standalone method call - // Check if this method is defined in the script itself if (isScriptMethod(methodName)) { resolvedMethod = getScriptMethodInfo(methodName); - - // Create MethodCallInfo for validation MethodCallInfo callInfo = new MethodCallInfo( methodName, nameStart, nameEnd, openParen, closeParen, arguments, null, resolvedMethod ); callInfo.validate(); - - // Store in collection for later lookup methodCalls.add(callInfo); - - // Always mark method call as green (METHOD_CALL), with error info attached for underline marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { - // Unknown standalone method - mark as undefined marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } } @@ -1532,7 +1511,7 @@ private List parseMethodArguments(int start, int end) { // Try to resolve the argument type TypeInfo argType = resolveExpressionType(argText, actualStart); - args.add(new MethodCallInfo.Argument( + args.add(new MethodCallInfo.Argument( argText, actualStart, actualEnd, argType, true, null )); } @@ -1564,8 +1543,17 @@ private List parseMethodArguments(int start, int end) { } /** - * Try to resolve the type of an expression (for argument type checking). - * This is a simplified version that handles common cases. + * Comprehensive expression type resolver that handles: + * - Literals (strings, numbers, booleans, null) + * - Variables (local, parameter, global) + * - Simple identifiers + * - Chained field accesses (a.b.c) + * - Chained method calls (a().b().c()) + * - Mixed chains (a.b().c.d()) + * - Static access (Class.field, Class.method()) + * - New expressions (new Type()) + * + * This is THE method that should be used for all type resolution needs. */ private TypeInfo resolveExpressionType(String expr, int position) { expr = expr.trim(); @@ -1611,18 +1599,9 @@ private TypeInfo resolveExpressionType(String expr, int position) { return TypeInfo.forPrimitive("double"); } - // Simple variable reference - if (expr.matches("[a-zA-Z_][a-zA-Z0-9_]*")) { - FieldInfo varInfo = resolveVariable(expr, position); - if (varInfo != null) { - return varInfo.getTypeInfo(); - } - } - // "this" keyword if (expr.equals("this")) { - // Could return the enclosing class type if we had that info - return null; + return findEnclosingScriptType(position); } // "new Type()" expressions @@ -1633,11 +1612,131 @@ private TypeInfo resolveExpressionType(String expr, int position) { } } - // Chain expressions like "obj.field" or "obj.method()" - // This is complex - for now, return null to indicate unknown - // The type compatibility check will be lenient with null types + // Now handle the complex case: chains of fields and method calls + // Parse the expression into segments + List segments = parseExpressionChain(expr); - return null; + if (segments.isEmpty()) { + return null; + } + + // Resolve the first segment + ChainSegment first = segments.get(0); + TypeInfo currentType = null; + + if (first.name.equals("this")) { + currentType = findEnclosingScriptType(position); + // Handle this.field where we don't have a script type + if (currentType == null && segments.size() > 1 && !segments.get(1).isMethodCall) { + String fieldName = segments.get(1).name; + if (globalFields.containsKey(fieldName)) { + currentType = globalFields.get(fieldName).getTypeInfo(); + // Continue from segment 2 + for (int i = 2; i < segments.size(); i++) { + currentType = resolveChainSegment(currentType, segments.get(i)); + if (currentType == null) return null; + } + return currentType; + } + } + } else if (Character.isUpperCase(first.name.charAt(0))) { + // Uppercase first letter - could be a class name for static access + currentType = resolveType(first.name); + } else if (!first.isMethodCall) { + // Regular variable + FieldInfo varInfo = resolveVariable(first.name, position); + if (varInfo != null) { + currentType = varInfo.getTypeInfo(); + } + } else { + // First segment is a method call - check script methods + if (isScriptMethod(first.name)) { + MethodInfo scriptMethod = getScriptMethodInfo(first.name); + if (scriptMethod != null) { + currentType = scriptMethod.getReturnType(); + } + } + } + + // Resolve the rest of the chain + for (int i = 1; i < segments.size(); i++) { + currentType = resolveChainSegment(currentType, segments.get(i)); + if (currentType == null) { + return null; + } + } + + return currentType; + } + + /** + * Parse an expression string into chain segments. + * Handles dots, method calls, and nested expressions. + * Examples: + * - "a.b.c" -> [a, b, c] (all fields) + * - "a().b()" -> [a(), b()] (all methods) + * - "a.b().c" -> [a, b(), c] (mixed) + */ + private List parseExpressionChain(String expr) { + List segments = new ArrayList<>(); + int i = 0; + + while (i < expr.length()) { + // Skip whitespace + while (i < expr.length() && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + if (i >= expr.length()) break; + + // Read identifier + int start = i; + while (i < expr.length() && Character.isJavaIdentifierPart(expr.charAt(i))) { + i++; + } + + if (i == start) { + // Not an identifier, skip this character + i++; + continue; + } + + String name = expr.substring(start, i); + + // Skip whitespace + while (i < expr.length() && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + // Check if followed by parentheses (method call) + boolean isMethodCall = false; + if (i < expr.length() && expr.charAt(i) == '(') { + isMethodCall = true; + // Skip to the matching closing paren + int depth = 1; + i++; + while (i < expr.length() && depth > 0) { + char c = expr.charAt(i); + if (c == '(') depth++; + else if (c == ')') depth--; + i++; + } + } + + segments.add(new ChainSegment(name, start, i, isMethodCall)); + + // Skip whitespace + while (i < expr.length() && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + // Check for dot (continuing chain) + if (i < expr.length() && expr.charAt(i) == '.') { + i++; // Skip the dot + } + } + + return segments; } /** @@ -1662,142 +1761,15 @@ private TypeInfo resolveReceiverChain(int methodNameStart) { return null; // No receiver } - // Walk backward to collect all segments in the chain - // Segments can be identifiers OR method calls (identifier followed by parentheses) - List chainSegments = new ArrayList<>(); - - int pos = scanPos; // Currently at the dot - - while (pos >= 0) { - // Skip the dot - pos--; - - // Skip whitespace - while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) - pos--; - - if (pos < 0) - break; - - char c = text.charAt(pos); - - // Check if this is the end of a method call (closing paren) - if (c == ')') { - // Find the matching opening paren - int closeParen = pos; - int openParen = findMatchingParenBackward(pos); - if (openParen < 0) { - return null; // Malformed - } - - // Now find the method name before the open paren - int beforeParen = openParen - 1; - while (beforeParen >= 0 && Character.isWhitespace(text.charAt(beforeParen))) - beforeParen--; - - if (beforeParen < 0 || !Character.isJavaIdentifierPart(text.charAt(beforeParen))) { - return null; // No method name - } - - // Read the method name backward - int methodNameEnd = beforeParen + 1; - while (beforeParen >= 0 && Character.isJavaIdentifierPart(text.charAt(beforeParen))) - beforeParen--; - int methodNameBegin = beforeParen + 1; - - String methodName = text.substring(methodNameBegin, methodNameEnd); - chainSegments.add(0, new ChainSegment(methodName, methodNameBegin, methodNameEnd, true)); - - pos = beforeParen; - } else if (Character.isJavaIdentifierPart(c)) { - // Regular identifier (field or variable) - int identEnd = pos + 1; - while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) - pos--; - int identStart = pos + 1; - - String ident = text.substring(identStart, identEnd); - chainSegments.add(0, new ChainSegment(ident, identStart, identEnd, false)); - } else { - // Unexpected character - end of chain - break; - } - - // Skip whitespace - while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) - pos--; - - // Check if there's another dot (continuing the chain) - if (pos >= 0 && text.charAt(pos) == '.') { - // Continue the loop to get the next segment - continue; - } else { - // No more dots - end of chain - break; - } - } - - if (chainSegments.isEmpty()) { - return null; - } - - // Now resolve the chain from left to right - ChainSegment firstSeg = chainSegments.get(0); - String firstSegment = firstSeg.name; - TypeInfo currentType = null; - - if (firstSegment.equals("this")) { - // For 'this', check if we're inside a script-defined class - currentType = findEnclosingScriptType(methodNameStart); - - if (currentType == null && chainSegments.size() > 1) { - // Fallback: this.something - check globalFields - ChainSegment nextSeg = chainSegments.get(1); - if (!nextSeg.isMethodCall && globalFields.containsKey(nextSeg.name)) { - currentType = globalFields.get(nextSeg.name).getTypeInfo(); - // Continue resolving from index 2 - for (int i = 2; i < chainSegments.size(); i++) { - currentType = resolveChainSegment(currentType, chainSegments.get(i)); - if (currentType == null) return null; - } - return currentType; - } - } - // If we have a script type, continue from index 1 - if (currentType != null) { - for (int i = 1; i < chainSegments.size(); i++) { - currentType = resolveChainSegment(currentType, chainSegments.get(i)); - if (currentType == null) return null; - } - return currentType; - } - return null; - } else if (Character.isUpperCase(firstSegment.charAt(0))) { - // Static access like Minecraft.getMinecraft() - the type is the class itself - currentType = resolveType(firstSegment); - } else if (!firstSeg.isMethodCall) { - // Variable like mc.thePlayer.worldObj - FieldInfo varInfo = resolveVariable(firstSegment, firstSeg.start); - if (varInfo != null) { - currentType = varInfo.getTypeInfo(); - } - } else { - // First segment is a method call without a receiver - check script methods - if (isScriptMethod(firstSegment)) { - MethodInfo scriptMethod = getScriptMethodInfo(firstSegment); - if (scriptMethod != null) { - currentType = scriptMethod.getReturnType(); - } - } - } - - // Resolve remaining segments - for (int i = 1; i < chainSegments.size(); i++) { - currentType = resolveChainSegment(currentType, chainSegments.get(i)); - if (currentType == null) return null; - } - - return currentType; + // Use the shared helper to locate the receiver expression before the dot, + // then delegate resolution to the comprehensive resolver. + int[] bounds = findReceiverBoundsBefore(scanPos); + if (bounds == null) return null; + int start = bounds[0]; + int end = bounds[1]; + String receiverExpr = text.substring(start, end).trim(); + if (receiverExpr.isEmpty()) return null; + return resolveExpressionType(receiverExpr, start); } /** @@ -1897,6 +1869,101 @@ private int findMatchingParenBackward(int closeParenPos) { return -1; // No matching paren found } + /** + * Find the matching opening bracket of a given closing bracket position. + * Supports any pair of open/close chars (e.g., '[' and ']'). + */ + private int findMatchingBracketBackward(int closePos, char openChar, char closeChar) { + if (closePos < 0 || closePos >= text.length() || text.charAt(closePos) != closeChar) { + return -1; + } + + int depth = 1; + boolean inString = false; + char stringChar = 0; + + for (int i = closePos - 1; i >= 0; i--) { + char c = text.charAt(i); + + // Handle string literals (backward) + if (!inString && (c == '"' || c == '\'')) { + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) backslashCount++; + if (backslashCount % 2 == 0) { + inString = true; + stringChar = c; + } + } else if (inString && c == stringChar) { + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) backslashCount++; + if (backslashCount % 2 == 0) { + inString = false; + } + } + + if (inString) continue; + + if (isExcluded(i)) continue; + + if (c == closeChar) depth++; + else if (c == openChar) { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + /** + * Given the position of a dot ('.') in `text`, find the bounds [start, end) + * of the receiver expression immediately to the left of the dot. The end + * returned will be the dot index (exclusive). Returns null if not found or malformed. + */ + private int[] findReceiverBoundsBefore(int dotIndex) { + if (dotIndex < 0 || dotIndex >= text.length() || text.charAt(dotIndex) != '.') { + return null; + } + + int pos = dotIndex - 1; + // Skip whitespace + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--; + if (pos < 0) return null; + + while (pos >= 0) { + char c = text.charAt(pos); + if (Character.isWhitespace(c)) { pos--; continue; } + + if (c == ')') { + int open = findMatchingParenBackward(pos); + if (open < 0) return null; + pos = open - 1; + continue; + } + + if (c == ']') { + int open = findMatchingBracketBackward(pos, '[', ']'); + if (open < 0) return null; + pos = open - 1; + continue; + } + + if (Character.isJavaIdentifierPart(c)) { + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) pos--; + // If it's part of a chained identifier (preceded by a dot), continue + if (pos >= 0 && text.charAt(pos) == '.') { pos--; continue; } + break; + } + + // Unexpected char - stop and treat as start after this + break; + } + + int start = pos + 1; + int end = dotIndex; // exclusive + if (start >= end) return null; + return new int[]{start, end}; + } + /** * Find the script-defined type that contains the given position. * Used for resolving 'this' references. @@ -2035,7 +2102,7 @@ private void markChainedFieldAccesses(List marks) { // Start with an identifier or 'this' followed by at least one dot and another identifier Pattern chainPattern = Pattern.compile("\\b(this|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); Matcher m = chainPattern.matcher(text); - + while (m.find()) { int chainStart = m.start(1); @@ -2199,6 +2266,103 @@ private void markChainedFieldAccesses(List marks) { } } } + + // Second pass: handle cases where the receiver is a method call or array access + // Example: getMinecraft().thePlayer -> initial chainPattern won't match because + // the left side isn't a simple identifier. Find ".identifier" occurrences where + // the char before the dot ends with ')' or ']' and resolve the receiver expression. + Pattern dotIdent = Pattern.compile("\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher md = dotIdent.matcher(text); + while (md.find()) { + int identStart = md.start(1); + int identEnd = md.end(1); + int dotPos = md.start(); + + // Skip if excluded or in import/package + if (isExcluded(identStart) || isInImportOrPackage(identStart)) + continue; + + // Find non-whitespace char before the dot + int before = dotPos - 1; + while (before >= 0 && Character.isWhitespace(text.charAt(before))) + before--; + if (before < 0) + continue; + + char bc = text.charAt(before); + // Only handle when left side ends with ')' or ']' (method call or array access) + if (bc != ')' && bc != ']') + continue; + + // Extract receiver bounds and resolve its type + int[] bounds = findReceiverBoundsBefore(dotPos); + if (bounds == null) + continue; + int recvStart = bounds[0]; + int recvEnd = bounds[1]; + + if (isExcluded(recvStart) || isInImportOrPackage(recvStart)) + continue; + + String receiverExpr = text.substring(recvStart, recvEnd).trim(); + if (receiverExpr.isEmpty()) + continue; + + TypeInfo receiverType = resolveExpressionType(receiverExpr, recvStart); + + // Now mark the first field after the dot and continue chaining + String firstField = md.group(1); + FieldInfo fInfo = null; + if (receiverType != null && receiverType.hasField(firstField)) { + fInfo = receiverType.getFieldInfo(firstField); + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, fInfo)); + } else { + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR)); + } + + // Attempt to continue the chain after this identifier + int pos = identEnd; + TypeInfo currentType = (fInfo != null) ? fInfo.getTypeInfo() : null; + while (pos < text.length()) { + // Skip whitespace + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + if (pos >= text.length() || text.charAt(pos) != '.') + break; + pos++; // skip dot + // Skip whitespace + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) + break; + + int nStart = pos; + while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) + pos++; + int nEnd = pos; + String seg = text.substring(nStart, nEnd); + + // Check if next segment is a method call (followed by '(') -> stop + int checkPosInner = nEnd; + while (checkPosInner < text.length() && Character.isWhitespace(text.charAt(checkPosInner))) + checkPosInner++; + if (checkPosInner < text.length() && text.charAt(checkPosInner) == '(') + break; + + if (isExcluded(nStart)) + break; + + if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) { + FieldInfo segInfo = currentType.getFieldInfo(seg); + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, segInfo)); + currentType = (segInfo != null) ? segInfo.getTypeInfo() : null; + } else { + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR)); + currentType = null; + } + } + } + } private void markImportedClassUsages(List marks) { From 5af57937187948277dab16d786cbbf41413fde2b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 23:47:18 +0200 Subject: [PATCH 013/337] Improved curly underline rendering --- .../client/gui/util/script/interpreter/ScriptLine.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 72ddcb809..2e33bb9ae 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -255,7 +255,7 @@ public void drawString(int x, int y, int defaultColor) { * @param width Width of the underline * @param color Color in ARGB format (e.g., 0xFFFF5555 for red) */ - private void drawCurlyUnderline(int x, int y, int width, int color) { + protected static void drawCurlyUnderline(int x, int y, int width, int color) { if (width <= 0) return; @@ -278,12 +278,12 @@ private void drawCurlyUnderline(int x, int y, int width, int color) { GL11.glBegin(GL11.GL_LINE_STRIP); // Wave parameters: 2 pixels amplitude, 4 pixels wavelength int waveHeight = 1; - int waveLength = 4; - for (int i = 0; i <= width; i++) { + float waveLength = 4f; + for (float i = -0.5f; i <= width - 1; i += 0.125f) { // Create a sine-like wave pattern double phase = (double) i / waveLength * Math.PI * 2; - float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.5f; - GL11.glVertex2f(x + i, y + yOffset); + float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.25f; + GL11.glVertex2f(x + i + 3f, y + yOffset); } GL11.glEnd(); From 2ff1b3a6cfd3dc4caa9762de68381a7e76036d0f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 24 Dec 2025 23:50:58 +0200 Subject: [PATCH 014/337] Improved arg count/type error underline rendering logic --- .../script/interpreter/MethodCallInfo.java | 20 +---- .../util/script/interpreter/ScriptLine.java | 6 +- .../gui/util/script/interpreter/Token.java | 89 +++++++++++++++++-- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java index a4b16eba1..f9440a492 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -202,19 +202,7 @@ public boolean hasArgCountError() { public boolean hasArgTypeError() { return !this.argumentTypeErrors.isEmpty(); } - - public boolean hasArgTypeError(Token t) { - for (ArgumentTypeError ate : this.argumentTypeErrors) { - if (ate.getArg().equals(t)) { - return true; - } - } - return false; - } - - public boolean hasArgCountError(Token t) { - return hasArgCountError() && t.isMethodCall(); - } + /** * Check if this is a static access error (underline method name). @@ -233,6 +221,9 @@ public void setArgTypeError(int argIndex, String message) { this.argumentTypeErrors.add(new ArgumentTypeError(arguments.get(argIndex), argIndex, message)); } + public List getArgumentTypeErrors() { + return argumentTypeErrors; + } public class ArgumentTypeError { private ErrorType type = ErrorType.WRONG_ARG_TYPE; @@ -298,14 +289,11 @@ public void validate() { if (!isTypeCompatible(argType, paramType)) { setArgTypeError(i, "Expected " + paramType.getSimpleName() + " but got " + argType.getSimpleName()); - return; } } else if (paramType == null) { setArgTypeError(i, "Parameter type of '" + para.getName() + "' is unresolved"); - return; } else if (argType == null) { setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'"); - return; } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 2e33bb9ae..081bb1c7f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -223,10 +223,8 @@ public void drawString(int x, int y, int defaultColor) { } // Track underline position if token has one - if (t.hasUnderline()) { - int currentY = y + ClientProxy.Font.height() - 1; - drawCurlyUnderline(currentX + 1, currentY, tokenWidth + 1, t.getUnderlineColor()); - } + t.drawUnderline(currentX, y + ClientProxy.Font.height() - 1); + // Append the colored token builder.append(COLOR_CHAR) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index 7c6851d69..d0d6f7ee6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter; +import noppes.npcs.client.ClientProxy; + /** * Represents a single token in the source code with its type and metadata. * Tokens are the atomic units of the syntax highlighting system. @@ -121,20 +123,91 @@ public static Token undefined(String text, int start, int end) { public ScriptLine getParentLine() { return parentLine; } public boolean hasUnderline() { - if (methodCallInfo != null) { - if (methodCallInfo.hasArgCountError(this)) - return true; - else if (methodCallInfo.hasArgTypeError(this)) { - return true; - } - } - return false; + return isMethodCall() && (methodCallInfo.hasArgTypeError() || methodCallInfo.hasArgCountError()); } public int getUnderlineColor() { return underlineColor; } + /** + * Draw underlines for this token's errors. + * @param x start pixel of this token + * @param y baseline pixel for underline + */ + public void drawUnderline(int x, int y) { + if (methodCallInfo == null) + return; + + if (methodCallInfo.hasArgCountError()) { + int tokenWidth = ClientProxy.Font.width(text); + ScriptLine.drawCurlyUnderline(x + 1, y, tokenWidth + 1, getUnderlineColor()); + return; + } + + if (methodCallInfo.hasArgTypeError()) { + // For arg type errors, underline the specific argument span(s). + // Compute pixel X for each argument by walking tokens on the same line + ScriptLine parent = getParentLine(); + if (parent == null) + return; + + for (MethodCallInfo.ArgumentTypeError error : methodCallInfo.getArgumentTypeErrors()) { + MethodCallInfo.Argument arg = error.getArg(); + int argStart = arg.getStartOffset(); + int argEnd = arg.getEndOffset(); + + // Skip args not on this line + if (!parent.containsPosition(argStart)) + continue; + + // Compute X position by summing token widths from this token up to argStart + int argX = x; + Token cur = this; + + // If this token starts after the arg (shouldn't normally happen), skip + if (cur.getGlobalStart() > argStart) + continue; + + while (cur != null && cur.getParentLine() == parent && cur.getGlobalEnd() <= argStart) { + argX += ClientProxy.Font.width(cur.getText()); + cur = cur.nextOnLine(); + } + + if (cur == null || cur.getParentLine() != parent) + continue; + + // Add partial width of the token containing the arg start + int overlapStart = Math.max(0, argStart - cur.getGlobalStart()); + if (overlapStart > 0) { + String part = cur.getText().substring(0, Math.min(overlapStart, cur.getText().length())); + argX += ClientProxy.Font.width(part); + } + + // Compute total width of the argument (may span multiple tokens) + int argWidth = 0; + Token cur2 = cur; + int remainingStart = argStart; + while (cur2 != null && cur2.getParentLine() == parent && remainingStart < argEnd) { + int segStart = Math.max(remainingStart, cur2.getGlobalStart()); + int segEnd = Math.min(argEnd, cur2.getGlobalEnd()); + if (segEnd > segStart) { + int localStart = segStart - cur2.getGlobalStart(); + int localEnd = segEnd - cur2.getGlobalStart(); + String segText = cur2.getText() + .substring(localStart, Math.min(localEnd, cur2.getText().length())); + argWidth += ClientProxy.Font.width(segText); + } + remainingStart = cur2.getGlobalEnd(); + cur2 = cur2.nextOnLine(); + } + + if (argWidth > 0) { + ScriptLine.drawCurlyUnderline(argX, y, argWidth, getUnderlineColor()); + } + } + } + } // ==================== SETTERS ==================== public void setType(TokenType type) { this.type = type; } From 76fe28e42ccc31d1e53d5de146657997e254dc34 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 00:58:37 +0200 Subject: [PATCH 015/337] Improved arg count/type error underline rendering logic again --- .../util/script/interpreter/ScriptLine.java | 2 +- .../gui/util/script/interpreter/Token.java | 71 +++++-------------- 2 files changed, 19 insertions(+), 54 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 081bb1c7f..26d8ec848 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -281,7 +281,7 @@ protected static void drawCurlyUnderline(int x, int y, int width, int color) { // Create a sine-like wave pattern double phase = (double) i / waveLength * Math.PI * 2; float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.25f; - GL11.glVertex2f(x + i + 3f, y + yOffset); + GL11.glVertex2f(x + i + 2f, y + yOffset); } GL11.glEnd(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index d0d6f7ee6..2de8c2703 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -136,12 +136,16 @@ public int getUnderlineColor() { * @param y baseline pixel for underline */ public void drawUnderline(int x, int y) { - if (methodCallInfo == null) + if (!isMethodCall() || methodCallInfo == null) return; - + if (methodCallInfo.hasArgCountError()) { - int tokenWidth = ClientProxy.Font.width(text); - ScriptLine.drawCurlyUnderline(x + 1, y, tokenWidth + 1, getUnderlineColor()); + try { + int tokenWidth = ClientProxy.Font.width(text); + ScriptLine.drawCurlyUnderline(x + 1, y, tokenWidth + 1, getUnderlineColor()); + } catch (Exception e) { + + } return; } @@ -149,61 +153,22 @@ public void drawUnderline(int x, int y) { // For arg type errors, underline the specific argument span(s). // Compute pixel X for each argument by walking tokens on the same line ScriptLine parent = getParentLine(); + if (parent == null) return; + String line = parent.getText(); for (MethodCallInfo.ArgumentTypeError error : methodCallInfo.getArgumentTypeErrors()) { MethodCallInfo.Argument arg = error.getArg(); int argStart = arg.getStartOffset(); - int argEnd = arg.getEndOffset(); - - // Skip args not on this line - if (!parent.containsPosition(argStart)) - continue; - - // Compute X position by summing token widths from this token up to argStart - int argX = x; - Token cur = this; - - // If this token starts after the arg (shouldn't normally happen), skip - if (cur.getGlobalStart() > argStart) - continue; - - while (cur != null && cur.getParentLine() == parent && cur.getGlobalEnd() <= argStart) { - argX += ClientProxy.Font.width(cur.getText()); - cur = cur.nextOnLine(); - } - - if (cur == null || cur.getParentLine() != parent) - continue; - - // Add partial width of the token containing the arg start - int overlapStart = Math.max(0, argStart - cur.getGlobalStart()); - if (overlapStart > 0) { - String part = cur.getText().substring(0, Math.min(overlapStart, cur.getText().length())); - argX += ClientProxy.Font.width(part); - } - - // Compute total width of the argument (may span multiple tokens) - int argWidth = 0; - Token cur2 = cur; - int remainingStart = argStart; - while (cur2 != null && cur2.getParentLine() == parent && remainingStart < argEnd) { - int segStart = Math.max(remainingStart, cur2.getGlobalStart()); - int segEnd = Math.min(argEnd, cur2.getGlobalEnd()); - if (segEnd > segStart) { - int localStart = segStart - cur2.getGlobalStart(); - int localEnd = segEnd - cur2.getGlobalStart(); - String segText = cur2.getText() - .substring(localStart, Math.min(localEnd, cur2.getText().length())); - argWidth += ClientProxy.Font.width(segText); - } - remainingStart = cur2.getGlobalEnd(); - cur2 = cur2.nextOnLine(); - } - - if (argWidth > 0) { - ScriptLine.drawCurlyUnderline(argX, y, argWidth, getUnderlineColor()); + try { + String beforeArg = line.substring(0, + argStart - parent.getGlobalStart()); + int beforeWidth = ClientProxy.Font.width(beforeArg); + int argWidth = ClientProxy.Font.width(arg.getText()); + ScriptLine.drawCurlyUnderline(x + beforeWidth, y, argWidth, getUnderlineColor()); + } catch (Exception e) { + } } } From 8bcbc4c7cd80984155123a6531ea572387bb71f8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 01:07:44 +0200 Subject: [PATCH 016/337] Fixed IndexOutOfBounds --- .../gui/util/script/interpreter/Token.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index 2de8c2703..55c60fac1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -140,12 +140,8 @@ public void drawUnderline(int x, int y) { return; if (methodCallInfo.hasArgCountError()) { - try { - int tokenWidth = ClientProxy.Font.width(text); - ScriptLine.drawCurlyUnderline(x + 1, y, tokenWidth + 1, getUnderlineColor()); - } catch (Exception e) { - - } + int tokenWidth = ClientProxy.Font.width(text); + ScriptLine.drawCurlyUnderline(x + 1, y, tokenWidth + 1, getUnderlineColor()); return; } @@ -160,16 +156,15 @@ public void drawUnderline(int x, int y) { for (MethodCallInfo.ArgumentTypeError error : methodCallInfo.getArgumentTypeErrors()) { MethodCallInfo.Argument arg = error.getArg(); - int argStart = arg.getStartOffset(); - try { - String beforeArg = line.substring(0, - argStart - parent.getGlobalStart()); - int beforeWidth = ClientProxy.Font.width(beforeArg); - int argWidth = ClientProxy.Font.width(arg.getText()); - ScriptLine.drawCurlyUnderline(x + beforeWidth, y, argWidth, getUnderlineColor()); - } catch (Exception e) { - - } + int lineArgStart = arg.getStartOffset() - parent.getGlobalStart(); + if (lineArgStart < 0 || lineArgStart >= line.length()) + continue; + + String beforeArg = line.substring(0, lineArgStart); + int beforeWidth = ClientProxy.Font.width(beforeArg); + int argWidth = ClientProxy.Font.width(arg.getText()); + ScriptLine.drawCurlyUnderline(x + beforeWidth, y, argWidth, getUnderlineColor()); + } } } From cbb5f3e2ae79e5127e920f2650e3b2adf4086ff4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 01:35:27 +0200 Subject: [PATCH 017/337] Made method call underlines work multi lines --- .../script/interpreter/ScriptDocument.java | 4 + .../util/script/interpreter/ScriptLine.java | 97 ++++++++++++++++++- .../gui/util/script/interpreter/Token.java | 39 -------- 3 files changed, 98 insertions(+), 42 deletions(-) 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 6f9d63fdd..78209f711 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 @@ -2631,6 +2631,10 @@ public List getMethods() { return Collections.unmodifiableList(methods); } + public List getMethodCalls() { + return Collections.unmodifiableList(methodCalls); + } + public Map getGlobalFields() { return Collections.unmodifiableMap(globalFields); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 26d8ec848..9274a998f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -221,9 +221,6 @@ public void drawString(int x, int y, int defaultColor) { builder.append(gapText); currentX += ClientProxy.Font.width(gapText); } - - // Track underline position if token has one - t.drawUnderline(currentX, y + ClientProxy.Font.height() - 1); // Append the colored token @@ -244,6 +241,100 @@ public void drawString(int x, int y, int defaultColor) { // Draw the text ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); + + // Draw underlines for method call arguments that intersect this line + drawMethodCallUnderlines(x, y + ClientProxy.Font.height() - 1); + } + + /** + * Draw underlines for all method call arguments that intersect this line. + * Uses the same token-walking logic as Token.drawUnderline but applies it + * to ALL method calls in the document, enabling multi-line underlines. + */ + private void drawMethodCallUnderlines(int lineStartX, int baselineY) { + if (parent == null) + return; + + ScriptDocument doc = parent; + String lineText = getText(); + int lineStart = getGlobalStart(); + int lineEnd = getGlobalEnd(); + + // Check all method calls in the document + for (MethodCallInfo call : doc.getMethodCalls()) { + // Skip method declarations that were erroneously recorded as calls. + boolean isDeclaration = false; + int methodStart = call.getMethodNameStart(); + for (MethodInfo mi : doc.getMethods()) { + //SKIP method declarations cause for some reason their underlines are drawn here + if (mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) { + isDeclaration = true; + break; + } + } + if (isDeclaration) + continue; + // Skip if this call doesn't intersect this line + if (call.getCloseParenOffset() < lineStart || call.getOpenParenOffset() > lineEnd) + continue; + + // Handle arg count errors (underline the method name) + if (call.hasArgCountError()) { + // Check if method name is on this line + if (methodStart >= lineStart && methodStart < lineEnd) { + int lineLocalStart = methodStart - lineStart; + if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { + String beforeMethod = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeMethod); + int methodWidth = ClientProxy.Font.width(call.getMethodName()); + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); + } + } + } + + // Handle arg type errors (underline specific arguments) + if (call.hasArgTypeError()) { + for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) { + MethodCallInfo.Argument arg = error.getArg(); + int argStart = arg.getStartOffset(); + int argEnd = arg.getEndOffset(); + + // Clip argument to this line's bounds + int clipStart = Math.max(argStart, lineStart); + int clipEnd = Math.min(argEnd, lineEnd); + + // Skip if argument doesn't intersect this line + if (clipStart >= clipEnd) + continue; + + // Convert to line-local coordinates + int lineLocalStart = clipStart - lineStart; + int lineLocalEnd = clipEnd - lineStart; + + // Bounds check + if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) + continue; + + // Compute pixel position using same logic as Token.drawUnderline + String beforeArg = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeArg); + + int argWidth; + if (lineLocalEnd > lineText.length()) { + // Argument extends past line end + argWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); + } else { + // Argument is fully on this line (or clipped) + String argTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); + argWidth = ClientProxy.Font.width(argTextOnLine); + } + + if (argWidth > 0) { + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, argWidth, 0xFF5555); + } + } + } + } } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index 55c60fac1..43143c252 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -129,45 +129,6 @@ public boolean hasUnderline() { public int getUnderlineColor() { return underlineColor; } - - /** - * Draw underlines for this token's errors. - * @param x start pixel of this token - * @param y baseline pixel for underline - */ - public void drawUnderline(int x, int y) { - if (!isMethodCall() || methodCallInfo == null) - return; - - if (methodCallInfo.hasArgCountError()) { - int tokenWidth = ClientProxy.Font.width(text); - ScriptLine.drawCurlyUnderline(x + 1, y, tokenWidth + 1, getUnderlineColor()); - return; - } - - if (methodCallInfo.hasArgTypeError()) { - // For arg type errors, underline the specific argument span(s). - // Compute pixel X for each argument by walking tokens on the same line - ScriptLine parent = getParentLine(); - - if (parent == null) - return; - String line = parent.getText(); - - for (MethodCallInfo.ArgumentTypeError error : methodCallInfo.getArgumentTypeErrors()) { - MethodCallInfo.Argument arg = error.getArg(); - int lineArgStart = arg.getStartOffset() - parent.getGlobalStart(); - if (lineArgStart < 0 || lineArgStart >= line.length()) - continue; - - String beforeArg = line.substring(0, lineArgStart); - int beforeWidth = ClientProxy.Font.width(beforeArg); - int argWidth = ClientProxy.Font.width(arg.getText()); - ScriptLine.drawCurlyUnderline(x + beforeWidth, y, argWidth, getUnderlineColor()); - - } - } - } // ==================== SETTERS ==================== public void setType(TokenType type) { this.type = type; } From f5cb81ea89346afaf61fd092ef9b6e7f9e4c4e10 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 01:35:53 +0200 Subject: [PATCH 018/337] Removed no longer used underline fields --- .../util/script/interpreter/ScriptLine.java | 59 ++++--------------- .../gui/util/script/interpreter/Token.java | 16 ----- 2 files changed, 11 insertions(+), 64 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 9274a998f..950ab8a9e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -152,12 +152,7 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume } else if (mark.metadata instanceof MethodInfo) { token.setMethodInfo((MethodInfo) mark.metadata); } else if (mark.metadata instanceof MethodCallInfo) { - MethodCallInfo callInfo = (MethodCallInfo) mark.metadata; - token.setMethodCallInfo(callInfo); - if (callInfo.getErrorType() != MethodCallInfo.ErrorType.NONE) { - // For other errors (arg count, static access), underline the method name - token.setUnderline(true, 0xFF5555); // Red wavy underline - } + token.setMethodCallInfo((MethodCallInfo) mark.metadata); } else if (mark.metadata instanceof ImportData) { token.setImportData((ImportData) mark.metadata); } @@ -399,19 +394,19 @@ public void drawStringHex(int x, int y, HexColorRenderer renderer) { // Draw any text before this token in default color if (tokenStart > lastIndex && tokenStart <= text.length()) { String gap = text.substring(lastIndex, tokenStart); - currentX = renderer.draw(gap, currentX, y, 0xFFFFFF, false, 0); + currentX = renderer.draw(gap, currentX, y, 0xFFFFFF); } // Draw the colored token (with underline if flagged) - currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor(), t.hasUnderline(), - t.getUnderlineColor()); + currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor() + ); lastIndex = tokenStart + t.getText().length(); } // Draw any remaining text in default color if (lastIndex < text.length()) { - renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF, false, 0); + renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); } } @@ -422,15 +417,14 @@ public void drawStringHex(int x, int y, HexColorRenderer renderer) { public interface HexColorRenderer { /** * Draw text at the specified position with the given color. - * @param text The text to draw - * @param x X position - * @param y Y position + * + * @param text The text to draw + * @param x X position + * @param y Y position * @param hexColor The text color in hex format - * @param underline Whether to draw an underline (wavy for errors) - * @param underlineColor The underline color (if underline is true) * @return The X position after drawing (for continuation) */ - int draw(String text, int x, int y, int hexColor, boolean underline, int underlineColor); + int draw(String text, int x, int y, int hexColor); } /** @@ -439,43 +433,12 @@ public interface HexColorRenderer { * @return A HexColorRenderer that can be passed to drawStringHex */ public static HexColorRenderer createDefaultHexRenderer() { - return (text, x, y, hexColor, underline, underlineColor) -> { + return (text, x, y, hexColor) -> { // Draw the text with hex color // Minecraft's font renderer uses ARGB, so add full alpha if not present int color = (hexColor & 0xFF000000) == 0 ? (0xFF000000 | hexColor) : hexColor; ClientProxy.Font.drawString(text, x, y, color); int textWidth = ClientProxy.Font.width(text); - - // Draw curly underline if needed - if (underline && textWidth > 0) { - int ulColor = (underlineColor & 0xFF000000) == 0 ? (0xFF000000 | underlineColor) : underlineColor; - - float a = ((ulColor >> 24) & 0xFF) / 255f; - float r = ((ulColor >> 16) & 0xFF) / 255f; - float g = ((ulColor >> 8) & 0xFF) / 255f; - float b = (ulColor & 0xFF) / 255f; - - GL11.glPushMatrix(); - GL11.glDisable(GL11.GL_TEXTURE_2D); - GL11.glEnable(GL11.GL_BLEND); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GL11.glColor4f(r, g, b, a); - GL11.glLineWidth(1.0f); - - int underlineY = y + ClientProxy.Font.height() - 1; - GL11.glBegin(GL11.GL_LINE_STRIP); - for (int i = 0; i <= textWidth; i++) { - double phase = (double) i / 4 * Math.PI * 2; - int yOffset = (int) (Math.sin(phase) * 2); - GL11.glVertex2f(x + i, underlineY + yOffset); - } - GL11.glEnd(); - - GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glDisable(GL11.GL_BLEND); - GL11.glPopMatrix(); - } - return x + textWidth; }; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index 43143c252..f55d906e6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -26,10 +26,6 @@ public class Token { private MethodCallInfo methodCallInfo; // For method calls with argument info private ImportData importData; // For import statements - // Rendering flags - private boolean hasUnderline; // True if this token should be underlined (for errors) - private int underlineColor = 0xFF5555; // Color of the underline (if any) - // Navigation - set by ScriptLine private Token prev; private Token next; @@ -122,13 +118,6 @@ public static Token undefined(String text, int start, int end) { public ImportData getImportData() { return importData; } public ScriptLine getParentLine() { return parentLine; } - public boolean hasUnderline() { - return isMethodCall() && (methodCallInfo.hasArgTypeError() || methodCallInfo.hasArgCountError()); - } - - public int getUnderlineColor() { - return underlineColor; - } // ==================== SETTERS ==================== public void setType(TokenType type) { this.type = type; } @@ -138,11 +127,6 @@ public int getUnderlineColor() { public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } - public void setUnderline(boolean hasUnderline, int color) { - this.hasUnderline = hasUnderline; - this.underlineColor = color; - } - void setParentLine(ScriptLine line) { this.parentLine = line; } void setPrev(Token prev) { this.prev = prev; } void setNext(Token next) { this.next = next; } From 92cffdf555d4a283683eece979660b86f3f3b1ba Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 11:31:42 +0200 Subject: [PATCH 019/337] Improved CTRL + Backspace logic --- .../client/gui/util/GuiScriptTextArea.java | 57 +++++++------------ 1 file changed, 22 insertions(+), 35 deletions(-) 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 bb77cfaad..18ab07b84 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1428,46 +1428,33 @@ private boolean handleDeletionKeys(int i) { // CTRL+BACKSPACE: delete to previous word or whitespace boundary. if (isKeyComboCtrlBackspace(i)) { - String s = getSelectionBeforeText(); + String before = getSelectionBeforeText(); if (selection.getStartSelection() > 0 && !selection.hasSelection()) { - int nearestCondition = selection.getCursorPosition(); - int g; - // If the char left of caret is whitespace, find the first non-space to the left; - // otherwise find first whitespace/newline to the left (word boundary). - boolean cursorInWhitespace = Character.isWhitespace(s.charAt(selection.getCursorPosition() - 1)); - if (cursorInWhitespace) { - // Scan left until non-whitespace (start of previous word) - for (g = selection.getCursorPosition() - 1; g >= 0; g--) { - char currentChar = s.charAt(g); - if (!Character.isWhitespace(currentChar)) { - nearestCondition = g; - break; - } - if (g == 0) { - nearestCondition = 0; - } - } + int pos = selection.getCursorPosition(); + int g = pos; + + // Helper: treat letters, digits and underscore as word characters + java.util.function.IntPredicate isWordChar = ch -> Character.isLetterOrDigit(ch) || ch == '_'; + + // If caret is after whitespace, delete contiguous whitespace first + char left = before.charAt(pos - 1); + if (Character.isWhitespace(left)) { + while (g - 1 >= 0 && Character.isWhitespace(before.charAt(g - 1))) + g--; + } else if (isWordChar.test(left)) { + // Delete contiguous word characters (letters/digits/_) + while (g - 1 >= 0 && isWordChar.test(before.charAt(g - 1))) + g--; } else { - // Scan left until whitespace/newline is found (word boundary) - for (g = selection.getCursorPosition() - 1; g >= 0; g--) { - char currentChar = s.charAt(g); - if (Character.isWhitespace(currentChar) || currentChar == '\n') { - nearestCondition = g; - break; - } - if (g == 0) { - nearestCondition = 0; - } - } + // Delete contiguous non-word, non-whitespace characters (punctuation) + while (g - 1 >= 0 && !Character.isWhitespace(before.charAt(g - 1)) && !isWordChar.test(before.charAt(g - 1))) + g--; } - // Trim the prefix up to the discovered boundary - s = s.substring(0, nearestCondition); - // Adjust selection start to match removed characters - selection.setStartSelection( - selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); + before = before.substring(0, g); + selection.setStartSelection(selection.getStartSelection() - (pos - g)); } - setText(s + getSelectionAfterText()); + setText(before + getSelectionAfterText()); selection.reset(selection.getStartSelection()); return true; } From b38f193d082a1c6d52372a7a56c6545170a2dddf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 11:35:39 +0200 Subject: [PATCH 020/337] Improved CTRL + Left/Right logic --- .../client/gui/util/GuiScriptTextArea.java | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) 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 18ab07b84..34b49d075 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1249,16 +1249,21 @@ private boolean handleNavigationKeys(int i) { if (i == Keyboard.KEY_LEFT) { int j = 1; // default: move one character if (isCtrlKeyDown()) { - // When Ctrl is down, compute distance to previous word boundary. - // We match words in the text slice before the cursor and pick - // the last match start as the new boundary. - Matcher m = container.regexWord.matcher(text.substring(0, selection.getCursorPosition())); - while (m.find()) { - if (m.start() == m.end()) - continue; // skip empty matches - // j becomes the number of chars to move left to reach word start - j = selection.getCursorPosition() - m.start(); + int pos = selection.getCursorPosition(); + int g = pos; + java.util.function.IntPredicate isWordChar = ch -> Character.isLetterOrDigit(ch) || ch == '_'; + + if (pos > 0) { + char left = text.charAt(pos - 1); + if (Character.isWhitespace(left)) { + while (g - 1 >= 0 && Character.isWhitespace(text.charAt(g - 1))) g--; + } else if (isWordChar.test(left)) { + while (g - 1 >= 0 && isWordChar.test(text.charAt(g - 1))) g--; + } else { + while (g - 1 >= 0 && !Character.isWhitespace(text.charAt(g - 1)) && !isWordChar.test(text.charAt(g - 1))) g--; + } } + j = Math.max(1, pos - g); } int newPos = Math.max(selection.getCursorPosition() - j, 0); // If Shift is held, extend selection; otherwise place caret. @@ -1270,23 +1275,21 @@ private boolean handleNavigationKeys(int i) { if (i == Keyboard.KEY_RIGHT) { int j = 1; // default: move one character if (isCtrlKeyDown()) { - String after = text.substring(selection.getCursorPosition()); - Matcher m = container.regexWord.matcher(after); - if (m.find()) { - if (m.start() == 0) { - // If the first match starts at 0 (cursor at word start), - // try to find the next match so we advance past the current word. - if (m.find()) - j = m.start(); - else - j = Math.max(1, after.length()); + int pos = selection.getCursorPosition(); + int end = pos; + java.util.function.IntPredicate isWordChar = ch -> Character.isLetterOrDigit(ch) || ch == '_'; + + if (pos < text.length()) { + char first = text.charAt(pos); + if (Character.isWhitespace(first)) { + while (end < text.length() && Character.isWhitespace(text.charAt(end))) end++; + } else if (isWordChar.test(first)) { + while (end < text.length() && isWordChar.test(text.charAt(end))) end++; } else { - j = m.start(); + while (end < text.length() && !Character.isWhitespace(text.charAt(end)) && !isWordChar.test(text.charAt(end))) end++; } - } else { - // No word match found after cursor -> jump to end - j = Math.max(1, after.length()); } + j = Math.max(1, end - pos); } int newPos = Math.min(selection.getCursorPosition() + j, text.length()); setCursor(newPos, GuiScreen.isShiftKeyDown()); From 78a6a29b1c9ae82f6b71ca4e5bc7ee27134ed617 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 25 Dec 2025 23:12:36 +0200 Subject: [PATCH 021/337] Validated right hand expression type is the expected type at variable declaration, and underlined the mismatched type's token if not. --- .../client/gui/util/GuiScriptTextArea.java | 1 + .../script/interpreter/FieldAccessInfo.java | 172 +++++++++++++ .../script/interpreter/MethodCallInfo.java | 26 ++ .../script/interpreter/ScriptDocument.java | 243 +++++++++++++++++- .../util/script/interpreter/ScriptLine.java | 52 +++- 5 files changed, 486 insertions(+), 8 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java 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 34b49d075..14f07e7eb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -839,6 +839,7 @@ public void drawTextBox(int xMouse, int yMouse) { // data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); scriptLine.drawString(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + // scriptLine.drawStringHex(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, ScriptLine.createDefaultHexRenderer()); // Draw cursor: pause blinking while user is active recently boolean recentInput = selection.hadRecentInput(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java new file mode 100644 index 000000000..4f169fd7c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java @@ -0,0 +1,172 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Metadata for field access validation. + * Parallel to MethodCallInfo but for field accesses like `obj.field` or `Class.staticField`. + */ +public class FieldAccessInfo { + + /** + * Validation error type for field accesses. + */ + public enum ErrorType { + NONE, + TYPE_MISMATCH, // Field type doesn't match expected type (e.g., assignment LHS) + UNRESOLVED_FIELD, // Field doesn't exist on the receiver type + STATIC_ACCESS_ERROR // Trying to access instance field statically or vice versa + } + + private final String fieldName; + private final int fieldNameStart; + private final int fieldNameEnd; + private final TypeInfo receiverType; // The type on which this field is accessed + private final FieldInfo resolvedField; // The resolved field (null if unresolved) + private final boolean isStaticAccess; // True if this is Class.field style access + private TypeInfo expectedType; // Expected field type (from assignment LHS, etc.) + + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + + public FieldAccessInfo(String fieldName, int fieldNameStart, int fieldNameEnd, + TypeInfo receiverType, FieldInfo resolvedField, boolean isStaticAccess) { + this.fieldName = fieldName; + this.fieldNameStart = fieldNameStart; + this.fieldNameEnd = fieldNameEnd; + this.receiverType = receiverType; + this.resolvedField = resolvedField; + this.isStaticAccess = isStaticAccess; + } + + // Getters + public String getFieldName() { + return fieldName; + } + + public int getFieldNameStart() { + return fieldNameStart; + } + + public int getFieldNameEnd() { + return fieldNameEnd; + } + + public TypeInfo getReceiverType() { + return receiverType; + } + + public FieldInfo getResolvedField() { + return resolvedField; + } + + public boolean isStaticAccess() { + return isStaticAccess; + } + + public TypeInfo getExpectedType() { + return expectedType; + } + + public void setExpectedType(TypeInfo expectedType) { + this.expectedType = expectedType; + } + + public ErrorType getErrorType() { + return errorType; + } + + public String getErrorMessage() { + return errorMessage; + } + + /** + * Validate this field access. + * Checks type compatibility with expected type. + */ + public void validate() { + // Check if field was resolved + if (resolvedField == null) { + setError(ErrorType.UNRESOLVED_FIELD, "Cannot resolve field '" + fieldName + "'"); + return; + } + + // Check return type compatibility with expected type (e.g., assignment LHS) + if (expectedType != null && resolvedField != null) { + TypeInfo fieldType = resolvedField.getDeclaredType(); + if (fieldType != null && !isTypeCompatible(fieldType, expectedType)) { + setError(ErrorType.TYPE_MISMATCH, + "Required type: " + expectedType.getSimpleName() + ", Provided: " + fieldType.getSimpleName()); + } + } + } + + /** + * Check if sourceType can be assigned to targetType. + */ + private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { + if (sourceType == null || targetType == null) { + return true; // Can't validate, assume compatible + } + + // Same type + if (sourceType.getFullName().equals(targetType.getFullName())) { + return true; + } + + // Check if sourceType is a subtype of targetType + if (sourceType.isResolved() && targetType.isResolved()) { + Class sourceClass = sourceType.getJavaClass(); + Class targetClass = targetType.getJavaClass(); + + if (sourceClass != null && targetClass != null) { + return targetClass.isAssignableFrom(sourceClass); + } + } + + // Primitive widening/boxing conversions would go here + // For now, just check direct equality + return false; + } + + private void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + /** + * Check if this field access has any validation error. + */ + public boolean hasError() { + return errorType != ErrorType.NONE; + } + + /** + * Check if this is a type mismatch error. + */ + public boolean hasTypeMismatch() { + return errorType == ErrorType.TYPE_MISMATCH; + } + + /** + * Check if this is an unresolved field error. + */ + public boolean hasUnresolvedField() { + return errorType == ErrorType.UNRESOLVED_FIELD; + } + + /** + * Check if this is a static access error. + */ + public boolean hasStaticAccessError() { + return errorType == ErrorType.STATIC_ACCESS_ERROR; + } + + @Override + public String toString() { + return "FieldAccessInfo{" + + "fieldName='" + fieldName + "', " + + "receiverType=" + receiverType + ", " + + "resolvedField=" + resolvedField + ", " + + "errorType=" + errorType + + '}'; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java index f9440a492..2d7d0afae 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -79,6 +79,7 @@ public enum ErrorType { WRONG_ARG_COUNT, // Number of args doesn't match any overload WRONG_ARG_TYPE, // Specific argument has wrong type STATIC_ACCESS_ERROR, // Trying to call instance method statically or vice versa + RETURN_TYPE_MISMATCH, // Return type doesn't match expected type (e.g., assignment LHS) UNRESOLVED_METHOD, // Method doesn't exist UNRESOLVED_RECEIVER // Can't resolve the receiver type } @@ -92,6 +93,7 @@ public enum ErrorType { private final TypeInfo receiverType; // The type on which this method is called (null for standalone) private final MethodInfo resolvedMethod; // The resolved method (null if unresolved) private final boolean isStaticAccess; // True if this is Class.method() style access + private TypeInfo expectedType; // Expected return type (from assignment LHS, etc.) private ErrorType errorType = ErrorType.NONE; private String errorMessage; @@ -162,6 +164,14 @@ public boolean isStaticAccess() { return isStaticAccess; } + public TypeInfo getExpectedType() { + return expectedType; + } + + public void setExpectedType(TypeInfo expectedType) { + this.expectedType = expectedType; + } + public ErrorType getErrorType() { return errorType; } @@ -211,6 +221,13 @@ public boolean hasStaticAccessError() { return errorType == ErrorType.STATIC_ACCESS_ERROR; } + /** + * Check if this is a return type mismatch error. + */ + public boolean hasReturnTypeMismatch() { + return errorType == ErrorType.RETURN_TYPE_MISMATCH; + } + // Setters for validation results public void setError(ErrorType type, String message) { this.errorType = type; @@ -296,6 +313,15 @@ public void validate() { setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'"); } } + + // Check return type compatibility with expected type (e.g., assignment LHS) + if (expectedType != null && resolvedMethod != null) { + TypeInfo returnType = resolvedMethod.getReturnType(); + if (returnType != null && !isTypeCompatible(returnType, expectedType)) { + setError(ErrorType.RETURN_TYPE_MISMATCH, + "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName()); + } + } } /** 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 78209f711..8b6c08c0c 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 @@ -76,6 +76,9 @@ public class ScriptDocument { // Method calls - stores all parsed method call information private final List methodCalls = new ArrayList<>(); + // Field accesses - stores all parsed field access information + private final List fieldAccesses = new ArrayList<>(); + // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); @@ -1322,6 +1325,15 @@ private void markMethodCalls(List marks) { methodName, nameStart, nameEnd, openParen, closeParen, arguments, receiverType, resolvedMethod, computedStaticAccess ); + + // Only set expected type if this is the final expression (not followed by .field or .method) + if (!isFollowedByDot(closeParen)) { + TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); + if (expectedType != null) { + callInfo.setExpectedType(expectedType); + } + } + callInfo.validate(); methodCalls.add(callInfo); marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); @@ -1339,6 +1351,15 @@ private void markMethodCalls(List marks) { methodName, nameStart, nameEnd, openParen, closeParen, arguments, null, resolvedMethod ); + + // Only set expected type if this is the final expression (not followed by .field or .method) + if (!isFollowedByDot(closeParen)) { + TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); + if (expectedType != null) { + callInfo.setExpectedType(expectedType); + } + } + callInfo.validate(); methodCalls.add(callInfo); marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); @@ -2247,7 +2268,29 @@ private void markChainedFieldAccesses(List marks) { //TODO: THIS MAY NEED TO HANDLE STATIC VS INSTANCE FIELDS WITH DIFFERENT RENDERING if (currentType.hasField(segment)) { FieldInfo fieldInfo = currentType.getFieldInfo(segment); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + + // Check if this is the last segment and if it's in an assignment context + boolean isLastSegment = (i == chainSegments.size() - 1); + if (isLastSegment) { + // Create FieldAccessInfo for validation + boolean isStaticAccess = Character.isUpperCase(chainSegments.get(i-1).charAt(0)); + FieldAccessInfo accessInfo = new FieldAccessInfo( + segment, segPos[0], segPos[1], currentType, fieldInfo, isStaticAccess + ); + + // Set expected type from assignment context + TypeInfo expectedType = findExpectedTypeAtPosition(segPos[0]); + if (expectedType != null) { + accessInfo.setExpectedType(expectedType); + } + + accessInfo.validate(); + fieldAccesses.add(accessInfo); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); + } else { + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + } + // Update currentType for next segment currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; @@ -2315,7 +2358,35 @@ private void markChainedFieldAccesses(List marks) { FieldInfo fInfo = null; if (receiverType != null && receiverType.hasField(firstField)) { fInfo = receiverType.getFieldInfo(firstField); - marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, fInfo)); + + // Check if there are more segments after this one + boolean hasMoreSegments = false; + int checkPos = identEnd; + while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos))) + checkPos++; + if (checkPos < text.length() && text.charAt(checkPos) == '.') { + hasMoreSegments = true; + } + + if (!hasMoreSegments) { + // This is the final field - create FieldAccessInfo for validation + boolean isStaticAccess = false; // Determine based on receiver + FieldAccessInfo accessInfo = new FieldAccessInfo( + firstField, identStart, identEnd, receiverType, fInfo, isStaticAccess + ); + + // Set expected type from assignment context + TypeInfo expectedType = findExpectedTypeAtPosition(identStart); + if (expectedType != null) { + accessInfo.setExpectedType(expectedType); + } + + accessInfo.validate(); + fieldAccesses.add(accessInfo); + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, accessInfo)); + } else { + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, fInfo)); + } } else { marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR)); } @@ -2352,9 +2423,38 @@ private void markChainedFieldAccesses(List marks) { if (isExcluded(nStart)) break; + // Check if this is the last segment (no more dots after) + boolean isLastSegment = true; + int checkNext = nEnd; + while (checkNext < text.length() && Character.isWhitespace(text.charAt(checkNext))) + checkNext++; + if (checkNext < text.length() && text.charAt(checkNext) == '.') { + isLastSegment = false; + } + if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) { FieldInfo segInfo = currentType.getFieldInfo(seg); - marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, segInfo)); + + if (isLastSegment) { + // Create FieldAccessInfo for validation + boolean isStaticAccess = false; + FieldAccessInfo accessInfo = new FieldAccessInfo( + seg, nStart, nEnd, currentType, segInfo, isStaticAccess + ); + + // Set expected type from assignment context + TypeInfo expectedType = findExpectedTypeAtPosition(nStart); + if (expectedType != null) { + accessInfo.setExpectedType(expectedType); + } + + accessInfo.validate(); + fieldAccesses.add(accessInfo); + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, accessInfo)); + } else { + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, segInfo)); + } + currentType = (segInfo != null) ? segInfo.getTypeInfo() : null; } else { marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR)); @@ -2570,6 +2670,14 @@ private boolean isFollowedByParen(int position) { return i < text.length() && text.charAt(i) == '('; } + private boolean isFollowedByDot(int position) { + // Start checking after the given position (e.g. after a closing paren) + int i = position + 1; + while (i < text.length() && Character.isWhitespace(text.charAt(i))) + i++; + return i < text.length() && text.charAt(i) == '.'; + } + private boolean isInImportOrPackage(int position) { if (position < 0 || position >= text.length()) return false; @@ -2635,6 +2743,10 @@ public List getMethodCalls() { return Collections.unmodifiableList(methodCalls); } + public List getFieldAccesses() { + return Collections.unmodifiableList(fieldAccesses); + } + public Map getGlobalFields() { return Collections.unmodifiableMap(globalFields); } @@ -2669,4 +2781,129 @@ public int getLineIndexAt(int globalPosition) { } return -1; } + + /** + * Find the expected type for an expression at the given position by looking for assignment context. + * Returns the type of the variable being assigned to, or null if not in an assignment. + * + * Examples: + * - "Type varName = expr;" -> returns Type + * - "varName = expr;" -> returns type of varName + */ + public TypeInfo findExpectedTypeAtPosition(int position) { + // Walk backward from position to find '=' + int pos = position - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + + // Look for '=' that precedes this position + int equalsPos = -1; + int depth = 0; // Track parentheses/brackets depth + for (int i = pos; i >= 0; i--) { + if (isExcluded(i)) continue; + + char c = text.charAt(i); + if (c == ')' || c == ']') depth++; + else if (c == '(' || c == '[') depth--; + else if (c == '=' && depth == 0) { + // Make sure it's not ==, !=, <=, >= + if (i > 0 && "!<>=".indexOf(text.charAt(i - 1)) >= 0) continue; + if (i < text.length() - 1 && text.charAt(i + 1) == '=') continue; + equalsPos = i; + break; + } + else if (c == ';' || c == '{' || c == '}') { + // Reached statement boundary without finding assignment + break; + } + } + + if (equalsPos < 0) { + return null; // Not in an assignment + } + + // Now parse the left-hand side of the assignment + // Could be: "Type varName = expr" or "varName = expr" + pos = equalsPos - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + + if (pos < 0) return null; + + // Find the variable name (work backward to find identifier) + int varNameEnd = pos + 1; + int varNameStart = pos; + while (varNameStart >= 0 && (Character.isJavaIdentifierPart(text.charAt(varNameStart)))) { + varNameStart--; + } + varNameStart++; // Move to first char of identifier + + if (varNameStart >= varNameEnd) return null; + + String varName = text.substring(varNameStart, varNameEnd); + + // Now check if there's a type declaration before the variable name + pos = varNameStart - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + + if (pos < 0) { + // Just "varName = expr" - look up varName in scope + return lookupVariableType(varName, position); + } + + // Check if this could be a type declaration (look for identifier before varName) + int typeEnd = pos + 1; + int typeStart = pos; + while (typeStart >= 0 && (Character.isJavaIdentifierPart(text.charAt(typeStart)) || + text.charAt(typeStart) == '<' || text.charAt(typeStart) == '>' || + text.charAt(typeStart) == '[' || text.charAt(typeStart) == ']' || + text.charAt(typeStart) == ',')) { + typeStart--; + } + typeStart++; + + if (typeStart >= typeEnd) { + // Just "varName = expr" - look up varName + return lookupVariableType(varName, position); + } + + String typeStr = text.substring(typeStart, typeEnd).trim(); + + // Check if this is a var/let/const keyword (type inference) + if (typeStr.equals("var") || typeStr.equals("let") || typeStr.equals("const")) { + // Can't determine expected type from var/let/const + return null; + } + + // Resolve the type + return resolveType(typeStr); + } + + /** + * Look up the type of a variable by name at the given position. + */ + private TypeInfo lookupVariableType(String varName, int position) { + // Check method locals + MethodInfo containingMethod = findMethodAtPosition(position); + if (containingMethod != null) { + Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); + if (locals != null && locals.containsKey(varName)) { + FieldInfo field = locals.get(varName); + if (field.isVisibleAt(position)) { + return field.getDeclaredType(); + } + } + } + + // Check global fields + if (globalFields.containsKey(varName)) { + return globalFields.get(varName).getDeclaredType(); + } + + return null; + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 950ab8a9e..1383891bd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -236,17 +236,19 @@ public void drawString(int x, int y, int defaultColor) { // Draw the text ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor); +// Minecraft.getMinecraft().fontRenderer.drawString(builder.toString(), x, y, defaultColor); - // Draw underlines for method call arguments that intersect this line - drawMethodCallUnderlines(x, y + ClientProxy.Font.height() - 1); + + // Draw underlines for validation errors (method calls and field accesses) + drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); } /** - * Draw underlines for all method call arguments that intersect this line. + * Draw underlines for all validation errors (method calls and field accesses) that intersect this line. * Uses the same token-walking logic as Token.drawUnderline but applies it - * to ALL method calls in the document, enabling multi-line underlines. + * to ALL validatable items in the document, enabling multi-line underlines. */ - private void drawMethodCallUnderlines(int lineStartX, int baselineY) { + private void drawErrorUnderlines(int lineStartX, int baselineY) { if (parent == null) return; @@ -287,6 +289,20 @@ private void drawMethodCallUnderlines(int lineStartX, int baselineY) { } } + // Handle return type mismatch (underline the method name) + if (call.hasReturnTypeMismatch()) { + // Check if method name is on this line + if (methodStart >= lineStart && methodStart < lineEnd) { + int lineLocalStart = methodStart - lineStart; + if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { + String beforeMethod = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeMethod); + int methodWidth = ClientProxy.Font.width(call.getMethodName()); + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); + } + } + } + // Handle arg type errors (underline specific arguments) if (call.hasArgTypeError()) { for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) { @@ -330,6 +346,30 @@ private void drawMethodCallUnderlines(int lineStartX, int baselineY) { } } } + + // Check all field accesses in the document + for (FieldAccessInfo access : doc.getFieldAccesses()) { + // Skip if this access doesn't intersect this line + int fieldStart = access.getFieldNameStart(); + int fieldEnd = access.getFieldNameEnd(); + + if (fieldEnd < lineStart || fieldStart > lineEnd) + continue; + + // Handle type mismatch errors (underline the field name) + if (access.hasTypeMismatch()) { + // Check if field name is on this line + if (fieldStart >= lineStart && fieldStart < lineEnd) { + int lineLocalStart = fieldStart - lineStart; + if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { + String beforeField = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeField); + int fieldWidth = ClientProxy.Font.width(access.getFieldName()); + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, fieldWidth, 0xFF5555); + } + } + } + } } /** @@ -408,6 +448,8 @@ public void drawStringHex(int x, int y, HexColorRenderer renderer) { if (lastIndex < text.length()) { renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); } + drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); + } /** From 72686b8f8661d8f657327d68b3425526144418a7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 01:24:40 +0200 Subject: [PATCH 022/337] First implementation of TokenHoverInfo --- .../client/gui/util/GuiScriptTextArea.java | 115 ++++ .../interpreter/ScriptTextContainer.java | 20 + .../script/interpreter/hover/HoverState.java | 279 ++++++++++ .../interpreter/hover/TokenHoverInfo.java | 522 ++++++++++++++++++ .../interpreter/hover/TokenHoverRenderer.java | 332 +++++++++++ 5 files changed, 1268 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java 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 14f07e7eb..955827613 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -13,6 +13,9 @@ // New interpreter system imports import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import noppes.npcs.client.gui.util.script.interpreter.Token; +import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; +import noppes.npcs.client.gui.util.script.interpreter.hover.TokenHoverRenderer; import noppes.npcs.client.key.impl.ScriptEditorKeys; import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; @@ -82,6 +85,7 @@ private int getPaddedLineCount() { // ==================== HELPER CLASS INSTANCES ==================== private final ScrollState scroll = new ScrollState(); private final SelectionState selection = new SelectionState(); + private final HoverState hoverState = new HoverState(); // ==================== UI COMPONENTS ==================== private int cursorCounter; @@ -503,6 +507,9 @@ public void drawTextBox(int xMouse, int yMouse) { scroll.initializeIfNeeded(scroll.getScrolledLine()); scroll.update(maxScroll); + // Update hover state for token tooltips + updateHoverState(xMouse, yMouse); + // Handle click-dragging for selection if (clicked) { clicked = Mouse.isButtonDown(0); @@ -872,6 +879,13 @@ public void drawTextBox(int xMouse, int yMouse) { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); + + // Draw hover tooltip (on top of everything) + if (hoverState.isTooltipVisible()) { + ScaledResolution sr = new ScaledResolution(Minecraft.getMinecraft(), + Minecraft.getMinecraft().displayWidth, Minecraft.getMinecraft().displayHeight); + TokenHoverRenderer.render(hoverState, sr.getScaledWidth(), sr.getScaledHeight()); + } } private void scissorViewport() { @@ -935,6 +949,107 @@ private int getCursorLineIndex() { return selection.getCursorLineIndex(container.lines, text != null ? text.length() : 0); } + /** + * Get the token at a specific screen position (mouse coordinates). + * Also returns the token's screen position and dimensions for tooltip placement. + * + * @param xMouse Screen X coordinate + * @param yMouse Screen Y coordinate + * @return Array of [Token, tokenScreenX, tokenScreenY, tokenWidth] or null if no token + */ + private Object[] getTokenAtScreenPosition(int xMouse, int yMouse) { + if (container == null || !(container instanceof ScriptTextContainer)) { + return null; + } + + ScriptTextContainer scriptContainer = (ScriptTextContainer) container; + + // Check if mouse is within the text viewport + int textAreaX = x + LINE_NUMBER_GUTTER_WIDTH + 1; + if (xMouse < textAreaX || xMouse > x + width || yMouse < y || yMouse > y + height) { + return null; + } + + // Adjust mouse position relative to text area + int relativeX = xMouse - textAreaX; + int relativeY = yMouse - y; + + // Account for fractional scrolling + double fracOffset = scroll.getFractionalOffset(); + double fracPixels = fracOffset * container.lineHeight; + double adjustedY = relativeY + fracPixels; + + // Find which line the mouse is over + int lineIdx = scroll.getScrolledLine() + (int)(adjustedY / container.lineHeight); + if (lineIdx < 0 || lineIdx >= container.lines.size()) { + return null; + } + + LineData lineData = container.lines.get(lineIdx); + String lineText = lineData.text; + + // Find which character position in the line + int charPos = 0; + int accumulatedWidth = 0; + for (int i = 0; i < lineText.length(); i++) { + int charWidth = ClientProxy.Font.width(String.valueOf(lineText.charAt(i))); + if (relativeX < accumulatedWidth + charWidth) { + charPos = i; + break; + } + accumulatedWidth += charWidth; + charPos = i + 1; + } + + // Convert to global position + int globalPos = lineData.start + Math.min(charPos, lineText.length()); + + // Get the token at this position + Token token = scriptContainer.getInterpreterTokenAt(globalPos); + if (token == null) { + return null; + } + + // Calculate token's screen position + int tokenLocalStart = token.getGlobalStart() - lineData.start; + int tokenLocalEnd = token.getGlobalEnd() - lineData.start; + tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, lineText.length())); + tokenLocalEnd = Math.max(0, Math.min(tokenLocalEnd, lineText.length())); + + int tokenScreenX = textAreaX + ClientProxy.Font.width(lineText.substring(0, tokenLocalStart)); + int tokenScreenY = y + (lineIdx - scroll.getScrolledLine()) * container.lineHeight - (int)fracPixels; + int tokenWidth = ClientProxy.Font.width(lineText.substring(tokenLocalStart, tokenLocalEnd)); + + return new Object[] { token, tokenScreenX, tokenScreenY, tokenWidth }; + } + + /** + * Update hover state based on current mouse position. + * Called every frame from drawTextBox. + */ + private void updateHoverState(int xMouse, int yMouse) { + // Don't show tooltips when not active, clicking, or when overlays are visible + if (!active || !isEnabled() || clicked || searchBar.isVisible() || + goToLineDialog.isVisible() || KEYS_OVERLAY.isVisible() || renameHandler.isActive()) { + hoverState.clearHover(); + return; + } + + // Get token at current mouse position + Object[] tokenInfo = getTokenAtScreenPosition(xMouse, yMouse); + + if (tokenInfo != null) { + Token token = (Token) tokenInfo[0]; + int tokenScreenX = (Integer) tokenInfo[1]; + int tokenScreenY = (Integer) tokenInfo[2]; + int tokenWidth = (Integer) tokenInfo[3]; + + hoverState.update(xMouse, yMouse, token, tokenScreenX, tokenScreenY, tokenWidth); + } else { + hoverState.update(xMouse, yMouse, null, 0, 0, 0); + } + } + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) // Only scrolls if cursor is outside the visible area 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 16e604d19..5c98ca2f5 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 @@ -154,6 +154,26 @@ public ScriptDocument getDocument() { return document; } + /** + * Get the interpreter Token at a specific global position in the text. + * Returns null if no token is at that position or if the interpreter is disabled. + * + * @param globalPosition Position in the document text + * @return The Token at that position, or null + */ + public noppes.npcs.client.gui.util.script.interpreter.Token getInterpreterTokenAt(int globalPosition) { + if (!USE_NEW_INTERPRETER || document == null) { + return null; + } + + ScriptLine line = document.getLineAt(globalPosition); + if (line == null) { + return null; + } + + return line.getTokenAt(globalPosition); + } + /** * Get method blocks for compatibility with existing code. * Creates MethodBlock-like objects from the new MethodInfo. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java new file mode 100644 index 000000000..05f403432 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java @@ -0,0 +1,279 @@ +package noppes.npcs.client.gui.util.script.interpreter.hover; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; +import noppes.npcs.client.gui.util.script.interpreter.Token; + +/** + * Manages the hover state for token tooltips in the script editor. + * + * Tracks: + * - Current mouse position + * - Hovered token (if any) + * - Hover duration for delayed tooltip display + * - Whether tooltip should be visible + * + * Implements a 500ms delay before showing tooltips, resetting when + * the mouse moves to a different token. + */ +public class HoverState { + + /** Minimum hover time (ms) before showing tooltip */ + private static final long HOVER_DELAY_MS = 500; + + // ==================== STATE ==================== + + /** Currently hovered token (null if none) */ + private Token hoveredToken; + + /** Time when hover on current token started */ + private long hoverStartTime; + + /** Whether the tooltip should currently be displayed */ + private boolean tooltipVisible; + + /** Cached hover info for the current token */ + private TokenHoverInfo hoverInfo; + + /** Last known mouse position */ + private int lastMouseX; + private int lastMouseY; + + /** Position of the hovered token (for tooltip positioning) */ + private int tokenScreenX; + private int tokenScreenY; + private int tokenWidth; + + // ==================== UPDATE ==================== + + /** + * Update the hover state based on current mouse position. + * Should be called every frame from drawTextBox. + * + * @param mouseX Current mouse X position + * @param mouseY Current mouse Y position + * @param document The script document + * @param viewportX X position of the text viewport (after gutter) + * @param viewportY Y position of the text viewport + * @param viewportWidth Width of the text viewport + * @param viewportHeight Height of the text viewport + * @param scrollOffset Current vertical scroll offset (in lines) + * @param lineHeight Height of each line in pixels + * @param gutterWidth Width of the line number gutter + */ + public void update(int mouseX, int mouseY, ScriptDocument document, + int viewportX, int viewportY, int viewportWidth, int viewportHeight, + float scrollOffset, int lineHeight, int gutterWidth) { + + lastMouseX = mouseX; + lastMouseY = mouseY; + + // Check if mouse is within the text viewport + if (mouseX < viewportX || mouseX > viewportX + viewportWidth || + mouseY < viewportY || mouseY > viewportY + viewportHeight) { + clearHover(); + return; + } + + if (document == null) { + clearHover(); + return; + } + + // Calculate which line the mouse is over + int relativeY = mouseY - viewportY; + int lineIndex = (int) (scrollOffset + (relativeY / (float) lineHeight)); + + // Get the line + ScriptLine line = null; + for (ScriptLine l : document.getLines()) { + if (l.getLineIndex() == lineIndex) { + line = l; + break; + } + } + + if (line == null) { + clearHover(); + return; + } + + // Calculate character position within the line + int relativeX = mouseX - viewportX; + int globalPos = line.getGlobalStart() + getCharacterIndexAtX(line, relativeX); + + // Find the token at this position + Token token = line.getTokenAt(globalPos); + + if (token == null) { + clearHover(); + return; + } + + // Check if this is a new token + if (token != hoveredToken) { + // New token - reset timer + hoveredToken = token; + hoverStartTime = System.currentTimeMillis(); + tooltipVisible = false; + hoverInfo = null; + + // Calculate token screen position for tooltip + calculateTokenPosition(line, token, viewportX, viewportY, scrollOffset, lineHeight); + } else { + // Same token - check if delay has elapsed + long elapsed = System.currentTimeMillis() - hoverStartTime; + if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) { + tooltipVisible = true; + hoverInfo = TokenHoverInfo.fromToken(token); + } + } + } + + /** + * Update the hover state with a specific token. + * Simplified version for when the token has already been found. + * + * @param mouseX Current mouse X position + * @param mouseY Current mouse Y position + * @param token The token at the mouse position (or null if none) + * @param tokenX Screen X position of the token + * @param tokenY Screen Y position of the token + * @param tokenW Width of the token in pixels + */ + public void update(int mouseX, int mouseY, Token token, int tokenX, int tokenY, int tokenW) { + lastMouseX = mouseX; + lastMouseY = mouseY; + + if (token == null) { + clearHover(); + return; + } + + // Check if this is a new token + if (token != hoveredToken) { + // New token - reset timer + hoveredToken = token; + hoverStartTime = System.currentTimeMillis(); + tooltipVisible = false; + hoverInfo = null; + + // Store token position + tokenScreenX = tokenX; + tokenScreenY = tokenY; + tokenWidth = tokenW; + } else { + // Same token - check if delay has elapsed + long elapsed = System.currentTimeMillis() - hoverStartTime; + if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) { + tooltipVisible = true; + hoverInfo = TokenHoverInfo.fromToken(token); + } + } + } + + /** + * Clear the current hover state. + */ + public void clearHover() { + if (hoveredToken != null) { + hoveredToken = null; + hoverStartTime = 0; + tooltipVisible = false; + hoverInfo = null; + } + } + + /** + * Force the tooltip to hide (e.g., when clicking). + */ + public void hideTooltip() { + tooltipVisible = false; + } + + // ==================== POSITION CALCULATION ==================== + + /** + * Get the character index within a line at the given X pixel position. + */ + private int getCharacterIndexAtX(ScriptLine line, int x) { + String text = line.getText(); + if (text == null || text.isEmpty()) return 0; + + int accumWidth = 0; + for (int i = 0; i < text.length(); i++) { + int charWidth = noppes.npcs.client.ClientProxy.Font.width(String.valueOf(text.charAt(i))); + if (accumWidth + charWidth / 2 > x) { + return i; + } + accumWidth += charWidth; + } + return text.length(); + } + + /** + * Calculate the screen position of a token for tooltip positioning. + */ + private void calculateTokenPosition(ScriptLine line, Token token, + int viewportX, int viewportY, + float scrollOffset, int lineHeight) { + // X position: calculate width of text before the token + String lineText = line.getText(); + int tokenLocalStart = token.getGlobalStart() - line.getGlobalStart(); + tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, lineText.length())); + + String textBefore = lineText.substring(0, tokenLocalStart); + tokenScreenX = viewportX + noppes.npcs.client.ClientProxy.Font.width(textBefore); + + // Y position: line position minus scroll + int lineY = line.getLineIndex(); + tokenScreenY = viewportY + (int) ((lineY - scrollOffset) * lineHeight); + + // Token width + tokenWidth = noppes.npcs.client.ClientProxy.Font.width(token.getText()); + } + + // ==================== GETTERS ==================== + + public boolean isTooltipVisible() { + return tooltipVisible && hoverInfo != null && hoverInfo.hasContent(); + } + + public TokenHoverInfo getHoverInfo() { + return hoverInfo; + } + + public Token getHoveredToken() { + return hoveredToken; + } + + public int getTokenScreenX() { + return tokenScreenX; + } + + public int getTokenScreenY() { + return tokenScreenY; + } + + public int getTokenWidth() { + return tokenWidth; + } + + public int getLastMouseX() { + return lastMouseX; + } + + public int getLastMouseY() { + return lastMouseY; + } + + /** + * Get the progress (0.0 to 1.0) of the hover delay. + * Can be used for fade-in animation. + */ + public float getHoverProgress() { + if (hoveredToken == null) return 0f; + long elapsed = System.currentTimeMillis() - hoverStartTime; + return Math.min(1.0f, elapsed / (float) HOVER_DELAY_MS); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java new file mode 100644 index 000000000..b624c4cf5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -0,0 +1,522 @@ +package noppes.npcs.client.gui.util.script.interpreter.hover; + +import noppes.npcs.client.gui.util.script.interpreter.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +/** + * Data class containing all displayable information for a token hover tooltip. + * Extracts and formats information from Token metadata for rendering. + * + * Supports display for: + * - Classes/Interfaces/Enums: Package, declaration with modifiers/extends/implements + * - Methods: Return type, signature, parameters, Javadoc + * - Fields: Type, containing class, declaration + * - Variables: Type, scope (local/parameter/global) + * - Errors: Validation errors, type mismatches + */ +public class TokenHoverInfo { + + // ==================== DISPLAY SECTIONS ==================== + + /** Package name (e.g., "net.minecraft.client") - shown in gray */ + private String packageName; + + /** Icon indicator (e.g., "C" for class, "I" for interface, "m" for method) */ + private String iconIndicator; + + /** Primary declaration line with syntax coloring */ + private List declaration = new ArrayList<>(); + + /** Javadoc/documentation comment lines */ + private List documentation = new ArrayList<>(); + + /** Error messages (shown in red) */ + private List errors = new ArrayList<>(); + + /** Additional info lines (e.g., "Variable 'x' is never used") */ + private List additionalInfo = new ArrayList<>(); + + /** The token this info was built from */ + private final Token token; + + // ==================== TEXT SEGMENT ==================== + + /** + * A colored segment of text within the declaration line. + */ + public static class TextSegment { + public final String text; + public final int color; + + public TextSegment(String text, int color) { + this.text = text; + this.color = color; + } + + // Predefined colors matching IntelliJ dark theme + public static final int COLOR_KEYWORD = 0xCC7832; // Orange for keywords/modifiers + public static final int COLOR_TYPE = 0x6897BB; // Blue for types + public static final int COLOR_CLASS = 0xA9B7C6; // Light gray for class names + public static final int COLOR_METHOD = 0xFFC66D; // Yellow for method names + public static final int COLOR_FIELD = 0x9876AA; // Purple for fields + public static final int COLOR_PARAM = 0xA9B7C6; // Light gray for parameters + public static final int COLOR_PACKAGE = 0x808080; // Gray for package + public static final int COLOR_DEFAULT = 0xA9B7C6; // Default text + public static final int COLOR_ERROR = 0xFF6B68; // Red for errors + public static final int COLOR_STRING = 0x6A8759; // Green for strings + public static final int COLOR_ANNOTATION = 0xBBB529; // Yellow-green for annotations + } + + // ==================== CONSTRUCTOR ==================== + + private TokenHoverInfo(Token token) { + this.token = token; + } + + // ==================== FACTORY METHOD ==================== + + /** + * Build hover info from a token based on its type and metadata. + */ + public static TokenHoverInfo fromToken(Token token) { + if (token == null) return null; + + TokenHoverInfo info = new TokenHoverInfo(token); + + // First, check for errors + info.extractErrors(token); + + // Then, extract type-specific information + switch (token.getType()) { + case IMPORTED_CLASS: + case CLASS_DECL: + case INTERFACE_DECL: + case ENUM_DECL: + case TYPE_DECL: + case NEW_TYPE: + info.extractClassInfo(token); + break; + + case METHOD_CALL: + info.extractMethodCallInfo(token); + break; + + case METHOD_DECL: + info.extractMethodDeclInfo(token); + break; + + case GLOBAL_FIELD: + info.extractGlobalFieldInfo(token); + break; + + case LOCAL_FIELD: + info.extractLocalFieldInfo(token); + break; + + case PARAMETER: + info.extractParameterInfo(token); + break; + + case UNDEFINED_VAR: + info.extractUndefinedInfo(token); + break; + + case KEYWORD: + case MODIFIER: + case STRING: + case NUMBER: + case COMMENT: + // These don't need hover info typically + return null; + + default: + // For other types, try to extract any available metadata + if (token.getTypeInfo() != null) { + info.extractClassInfo(token); + } else if (token.getMethodInfo() != null) { + info.extractMethodDeclInfo(token); + } else if (token.getFieldInfo() != null) { + info.extractFieldInfoGeneric(token); + } else { + return null; // Nothing to show + } + break; + } + + return info; + } + + // ==================== EXTRACTION METHODS ==================== + + private void extractErrors(Token token) { + MethodCallInfo callInfo = token.getMethodCallInfo(); + if (callInfo != null) { + if (callInfo.hasArgCountError()) { + errors.add("Expected " + getExpectedArgCount(callInfo) + " argument(s) but found " + callInfo.getArguments().size()); + } + if (callInfo.hasArgTypeError()) { + for (MethodCallInfo.ArgumentTypeError error : callInfo.getArgumentTypeErrors()) { + errors.add(error.getMessage()); + } + } + if (callInfo.hasReturnTypeMismatch()) { + errors.add(callInfo.getErrorMessage()); + } + if (callInfo.hasStaticAccessError()) { + errors.add(callInfo.getErrorMessage()); + } + } + + FieldInfo fieldInfo = token.getFieldInfo(); + if (fieldInfo != null && !fieldInfo.isResolved()) { + errors.add("Cannot resolve symbol '" + token.getText() + "'"); + } + } + + private int getExpectedArgCount(MethodCallInfo callInfo) { + MethodInfo method = callInfo.getResolvedMethod(); + if (method != null) { + return method.getParameterCount(); + } + return 0; + } + + private void extractClassInfo(Token token) { + TypeInfo typeInfo = token.getTypeInfo(); + if (typeInfo == null) return; + + packageName = typeInfo.getPackageName(); + + Class clazz = typeInfo.getJavaClass(); + if (clazz != null) { + // Icon + if (clazz.isInterface()) { + iconIndicator = "I"; + } else if (clazz.isEnum()) { + iconIndicator = "E"; + } else { + iconIndicator = "C"; + } + + // Build declaration + int mods = clazz.getModifiers(); + + // Modifiers + if (Modifier.isPublic(mods)) addSegment("public ", TextSegment.COLOR_KEYWORD); + if (Modifier.isAbstract(mods) && !clazz.isInterface()) addSegment("abstract ", TextSegment.COLOR_KEYWORD); + if (Modifier.isFinal(mods)) addSegment("final ", TextSegment.COLOR_KEYWORD); + + // Class type keyword + if (clazz.isInterface()) { + addSegment("interface ", TextSegment.COLOR_KEYWORD); + } else if (clazz.isEnum()) { + addSegment("enum ", TextSegment.COLOR_KEYWORD); + } else { + addSegment("class ", TextSegment.COLOR_KEYWORD); + } + + // Class name + addSegment(typeInfo.getSimpleName(), TextSegment.COLOR_CLASS); + + // Extends + Class superclass = clazz.getSuperclass(); + if (superclass != null && superclass != Object.class) { + addSegment(" extends ", TextSegment.COLOR_KEYWORD); + int superClassCol = TextSegment.COLOR_TYPE; + if (superclass.isInterface()) { + superClassCol = TokenType.INTERFACE_DECL.getHexColor(); + } else if (superclass.isEnum()) { + superClassCol = TokenType.ENUM_DECL.getHexColor(); + } else { + superClassCol = TokenType.IMPORTED_CLASS.getHexColor(); + + } + addSegment(superclass.getSimpleName(), superClassCol); + } + + List declaration = null; + + // Implements + Class[] interfaces = clazz.getInterfaces(); + if (interfaces.length > 0) { + addSegment(clazz.isInterface() ? " extends " : " implements ", TextSegment.COLOR_KEYWORD); + for (int i = 0; i < Math.min(interfaces.length, 3); i++) { + if (i > 0) addSegment(", ", TextSegment.COLOR_DEFAULT); + addSegment(interfaces[i].getSimpleName(), TokenType.INTERFACE_DECL.getHexColor()); + } + if (interfaces.length > 3) { + addSegment(", ...", TextSegment.COLOR_DEFAULT); + } + } + } else { + // Unresolved type + iconIndicator = "?"; + addSegment(typeInfo.getSimpleName(), TextSegment.COLOR_CLASS); + if (!typeInfo.isResolved()) { + errors.add("Cannot resolve class '" + typeInfo.getSimpleName() + "'"); + } + } + } + + private void extractMethodCallInfo(Token token) { + MethodCallInfo callInfo = token.getMethodCallInfo(); + MethodInfo methodInfo = callInfo != null ? callInfo.getResolvedMethod() : token.getMethodInfo(); + + if (methodInfo == null && callInfo == null) return; + + iconIndicator = "m"; + + TypeInfo containingType = null; + if (callInfo != null) { + containingType = callInfo.getReceiverType(); + } + if (containingType == null && methodInfo != null) { + containingType = methodInfo.getContainingType(); + } + + if (containingType != null) { + // Show full package.ClassName for context (like IntelliJ) + String pkg = containingType.getPackageName(); + String className = containingType.getSimpleName(); + if (pkg != null && !pkg.isEmpty()) { + packageName = pkg + "." + className; + } else { + packageName = className; + } + } + + // Try to get actual Java method for more details + if (containingType != null && containingType.getJavaClass() != null && methodInfo != null) { + Class clazz = containingType.getJavaClass(); + Method javaMethod = findJavaMethod(clazz, methodInfo.getName(), methodInfo.getParameterCount()); + + if (javaMethod != null) { + buildMethodDeclaration(javaMethod, containingType); + extractJavadoc(javaMethod); + return; + } + } + + // Fallback to basic method info + if (methodInfo != null) { + buildBasicMethodDeclaration(methodInfo, containingType); + } + } + + private void extractMethodDeclInfo(Token token) { + MethodInfo methodInfo = token.getMethodInfo(); + if (methodInfo == null) return; + + iconIndicator = "m"; + + // For script-defined methods, show basic declaration + buildBasicMethodDeclaration(methodInfo, null); + } + + private void extractGlobalFieldInfo(Token token) { + FieldInfo fieldInfo = token.getFieldInfo(); + if (fieldInfo == null) return; + + iconIndicator = "f"; + + TypeInfo declaredType = fieldInfo.getDeclaredType(); + if (declaredType != null) { + // Show field's type package.ClassName for context + String pkg = declaredType.getPackageName(); + String className = declaredType.getSimpleName(); + if (pkg != null && !pkg.isEmpty()) { + packageName = pkg; + } + + // Type + addSegment(declaredType.getSimpleName(), TextSegment.COLOR_TYPE); + addSegment(" ", TextSegment.COLOR_DEFAULT); + } + + // Field name + addSegment(fieldInfo.getName(), TextSegment.COLOR_FIELD); + } + + private void extractLocalFieldInfo(Token token) { + FieldInfo fieldInfo = token.getFieldInfo(); + if (fieldInfo == null) return; + + iconIndicator = "v"; + + TypeInfo declaredType = fieldInfo.getDeclaredType(); + if (declaredType != null) { + addSegment(declaredType.getSimpleName(), TextSegment.COLOR_TYPE); + addSegment(" ", TextSegment.COLOR_DEFAULT); + } + + addSegment(fieldInfo.getName(), TextSegment.COLOR_FIELD); + + // Show it's a local variable + additionalInfo.add("Local variable"); + } + + private void extractParameterInfo(Token token) { + FieldInfo fieldInfo = token.getFieldInfo(); + if (fieldInfo == null) return; + + iconIndicator = "p"; + + TypeInfo declaredType = fieldInfo.getDeclaredType(); + if (declaredType != null) { + addSegment(declaredType.getSimpleName(), TextSegment.COLOR_TYPE); + addSegment(" ", TextSegment.COLOR_DEFAULT); + } + + addSegment(fieldInfo.getName(), TextSegment.COLOR_PARAM); + + additionalInfo.add("Parameter"); + } + + private void extractUndefinedInfo(Token token) { + iconIndicator = "?"; + addSegment(token.getText(), TextSegment.COLOR_ERROR); + errors.add("Cannot resolve symbol '" + token.getText() + "'"); + } + + private void extractFieldInfoGeneric(Token token) { + FieldInfo fieldInfo = token.getFieldInfo(); + if (fieldInfo == null) return; + + switch (fieldInfo.getScope()) { + case GLOBAL: + extractGlobalFieldInfo(token); + break; + case LOCAL: + extractLocalFieldInfo(token); + break; + case PARAMETER: + extractParameterInfo(token); + break; + } + } + + // ==================== HELPER METHODS ==================== + + private void addSegment(String text, int color) { + declaration.add(new TextSegment(text, color)); + } + + private Method findJavaMethod(Class clazz, String name, int paramCount) { + try { + for (Method m : clazz.getMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == paramCount) { + return m; + } + } + } catch (Exception e) { + // Ignore + } + return null; + } + + private void buildMethodDeclaration(Method method, TypeInfo containingType) { + int mods = method.getModifiers(); + + // Annotations (show @Contract if present, etc.) + // Skip for now - could add later + + // Modifiers + if (Modifier.isPublic(mods)) addSegment("public ", TextSegment.COLOR_KEYWORD); + else if (Modifier.isProtected(mods)) addSegment("protected ", TextSegment.COLOR_KEYWORD); + else if (Modifier.isPrivate(mods)) addSegment("private ", TextSegment.COLOR_KEYWORD); + + if (Modifier.isStatic(mods)) addSegment("static ", TextSegment.COLOR_KEYWORD); + if (Modifier.isFinal(mods)) addSegment("final ", TextSegment.COLOR_KEYWORD); + if (Modifier.isAbstract(mods)) addSegment("abstract ", TextSegment.COLOR_KEYWORD); + if (Modifier.isSynchronized(mods)) addSegment("synchronized ", TextSegment.COLOR_KEYWORD); + + // Return type + Class returnType = method.getReturnType(); + addSegment(returnType.getSimpleName(), TextSegment.COLOR_TYPE); + addSegment(" ", TextSegment.COLOR_DEFAULT); + + // Method name + addSegment(method.getName(), TextSegment.COLOR_METHOD); + + // Parameters + addSegment("(", TextSegment.COLOR_DEFAULT); + Class[] paramTypes = method.getParameterTypes(); + java.lang.reflect.Parameter[] params = method.getParameters(); + for (int i = 0; i < paramTypes.length; i++) { + if (i > 0) addSegment(", ", TextSegment.COLOR_DEFAULT); + addSegment(paramTypes[i].getSimpleName(), TextSegment.COLOR_TYPE); + addSegment(" ", TextSegment.COLOR_DEFAULT); + // Try to get parameter name if available + String paramName = params.length > i ? params[i].getName() : "arg" + i; + addSegment(paramName, TextSegment.COLOR_PARAM); + } + addSegment(")", TextSegment.COLOR_DEFAULT); + } + + private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo containingType) { + // Modifiers + if (methodInfo.isStatic()) { + addSegment("static ", TextSegment.COLOR_KEYWORD); + } + + // Return type + TypeInfo returnType = methodInfo.getReturnType(); + if (returnType != null) { + addSegment(returnType.getSimpleName(), TextSegment.COLOR_TYPE); + addSegment(" ", TextSegment.COLOR_DEFAULT); + } else { + addSegment("void ", TextSegment.COLOR_KEYWORD); + } + + // Method name + addSegment(methodInfo.getName(), TextSegment.COLOR_METHOD); + + // Parameters + addSegment("(", TextSegment.COLOR_DEFAULT); + List params = methodInfo.getParameters(); + for (int i = 0; i < params.size(); i++) { + if (i > 0) addSegment(", ", TextSegment.COLOR_DEFAULT); + FieldInfo param = params.get(i); + TypeInfo paramType = param.getDeclaredType(); + if (paramType != null) { + addSegment(paramType.getSimpleName(), TextSegment.COLOR_TYPE); + addSegment(" ", TextSegment.COLOR_DEFAULT); + } + addSegment(param.getName(), TextSegment.COLOR_PARAM); + } + addSegment(")", TextSegment.COLOR_DEFAULT); + } + + private void extractJavadoc(Method method) { + // Java reflection doesn't provide Javadoc at runtime + // We could potentially load it from source files or external documentation + // For now, we'll leave this as a placeholder for future enhancement + + // Check for @Deprecated annotation + if (method.isAnnotationPresent(Deprecated.class)) { + additionalInfo.add("@Deprecated"); + } + } + + // ==================== GETTERS ==================== + + public String getPackageName() { return packageName; } + public String getIconIndicator() { return iconIndicator; } + public List getDeclaration() { return declaration; } + public List getDocumentation() { return documentation; } + public List getErrors() { return errors; } + public List getAdditionalInfo() { return additionalInfo; } + public Token getToken() { return token; } + + public boolean hasContent() { + return !declaration.isEmpty() || !errors.isEmpty() || !documentation.isEmpty(); + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java new file mode 100644 index 000000000..08181ff6b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -0,0 +1,332 @@ +package noppes.npcs.client.gui.util.script.interpreter.hover; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.ScaledResolution; +import noppes.npcs.client.ClientProxy; +import org.lwjgl.opengl.GL11; + +import java.util.List; + +/** + * Renders hover tooltips for tokens in the script editor. + * + * Renders IntelliJ-style tooltips with: + * - Package name in gray + * - Icon indicator (C/I/E/m/f/v/p) + * - Colored declaration line + * - Documentation/Javadoc + * - Error messages in red + * + * Handles positioning to keep tooltip on screen. + */ +public class TokenHoverRenderer { + + // ==================== CONSTANTS ==================== + + /** Padding inside the tooltip box */ + private static final int PADDING = 6; + + /** Gap between icon and text */ + private static final int ICON_GAP = 4; + + /** Icon box size */ + private static final int ICON_SIZE = 12; + + /** Line spacing */ + private static final int LINE_SPACING = 2; + + /** Vertical offset from token */ + private static final int VERTICAL_OFFSET = 4; + + /** Maximum tooltip width */ + private static final int MAX_WIDTH = 400; + + /** Minimum tooltip width */ + private static final int MIN_WIDTH = 150; + + // ==================== COLORS ==================== + + /** Background color (dark gray like IntelliJ) */ + private static final int BG_COLOR = 0xF0313335; + + /** Border color */ + private static final int BORDER_COLOR = 0xFF3C3F41; + + /** Package text color */ + private static final int PACKAGE_COLOR = 0xFF808080; + + /** Error text color */ + private static final int ERROR_COLOR = 0xFFFF6B68; + + /** Info text color */ + private static final int INFO_COLOR = 0xFF808080; + + // ==================== ICON COLORS ==================== + + private static final int ICON_CLASS_BG = 0xFF4A6B8A; // Blue for classes + private static final int ICON_INTERFACE_BG = 0xFF8A6B4A; // Orange-brown for interfaces + private static final int ICON_ENUM_BG = 0xFF6B8A4A; // Green for enums + private static final int ICON_METHOD_BG = 0xFF8A4A6B; // Purple for methods + private static final int ICON_FIELD_BG = 0xFF4A8A6B; // Teal for fields + private static final int ICON_VAR_BG = 0xFF6B6B8A; // Blue-gray for variables + private static final int ICON_PARAM_BG = 0xFF8A8A4A; // Yellow for parameters + private static final int ICON_UNKNOWN_BG = 0xFF8A4A4A; // Red for unknown + + // ==================== RENDERING ==================== + + /** + * Render the hover tooltip. + * + * @param hoverState The current hover state + * @param screenWidth Screen width for positioning + * @param screenHeight Screen height for positioning + */ + public static void render(HoverState hoverState, int screenWidth, int screenHeight) { + if (!hoverState.isTooltipVisible()) return; + + TokenHoverInfo info = hoverState.getHoverInfo(); + if (info == null || !info.hasContent()) return; + + // Calculate content dimensions + int contentWidth = calculateContentWidth(info); + int contentHeight = calculateContentHeight(info); + + // Add padding + int boxWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, contentWidth + PADDING * 2 + ICON_SIZE + ICON_GAP)); + int boxHeight = contentHeight + PADDING * 2; + + // Position the tooltip + int tokenX = hoverState.getTokenScreenX(); + int tokenY = hoverState.getTokenScreenY(); + int lineHeight = ClientProxy.Font.height(); + + // Default: show below the token + int tooltipX = tokenX; + int tooltipY = tokenY + lineHeight + VERTICAL_OFFSET; + + // Adjust if tooltip would go off-screen + if (tooltipX + boxWidth > screenWidth - 10) { + tooltipX = screenWidth - boxWidth - 10; + } + if (tooltipX < 10) { + tooltipX = 10; + } + + if (tooltipY + boxHeight > screenHeight - 10) { + // Show above the token instead + tooltipY = tokenY - boxHeight - VERTICAL_OFFSET; + } + if (tooltipY < 10) { + tooltipY = 10; + } + + // Render the tooltip + renderTooltipBox(tooltipX, tooltipY, boxWidth, boxHeight, info); + } + + /** + * Render the tooltip box with all content. + */ + private static void renderTooltipBox(int x, int y, int width, int height, TokenHoverInfo info) { + // Disable scissor test temporarily for tooltip rendering + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Draw background + Gui.drawRect(x, y, x + width, y + height, BG_COLOR); + + // Draw border + Gui.drawRect(x, y, x + width, y + 1, BORDER_COLOR); + Gui.drawRect(x, y + height - 1, x + width, y + height, BORDER_COLOR); + Gui.drawRect(x, y, x + 1, y + height, BORDER_COLOR); + Gui.drawRect(x + width - 1, y, x + width, y + height, BORDER_COLOR); + + int contentX = x + PADDING; + int contentY = y + PADDING; + int textX = contentX + ICON_SIZE + ICON_GAP; + int maxTextWidth = width - PADDING * 2 - ICON_SIZE - ICON_GAP; + + // Draw icon + String icon = info.getIconIndicator(); + if (icon != null && !icon.isEmpty()) { + drawIcon(contentX, contentY, icon); + } + + int currentY = contentY; + + // Draw errors first (if any) + List errors = info.getErrors(); + if (!errors.isEmpty()) { + for (String error : errors) { + drawText(textX, currentY, error, ERROR_COLOR); + currentY += ClientProxy.Font.height() + LINE_SPACING; + } + currentY += LINE_SPACING; // Extra space after errors + } + + // Draw package name + String packageName = info.getPackageName(); + if (packageName != null && !packageName.isEmpty()) { + // Draw package icon + drawText(textX, currentY, "\u25CB " + packageName, PACKAGE_COLOR); + currentY += ClientProxy.Font.height() + LINE_SPACING; + } + + // Draw declaration + List declaration = info.getDeclaration(); + if (!declaration.isEmpty()) { + int segmentX = textX; + for (TokenHoverInfo.TextSegment segment : declaration) { + int segmentWidth = ClientProxy.Font.width(segment.text); + + // Check if we need to wrap (simple wrapping for now) + if (segmentX + segmentWidth > x + width - PADDING && segmentX > textX) { + currentY += ClientProxy.Font.height() + LINE_SPACING; + segmentX = textX; + } + + drawText(segmentX, currentY, segment.text, 0xFF000000 | segment.color); + segmentX += segmentWidth; + } + currentY += ClientProxy.Font.height() + LINE_SPACING; + } + + // Draw documentation + List docs = info.getDocumentation(); + if (!docs.isEmpty()) { + currentY += LINE_SPACING; // Extra space before docs + for (String doc : docs) { + drawText(textX, currentY, doc, 0xFFA9B7C6); + currentY += ClientProxy.Font.height() + LINE_SPACING; + } + } + + // Draw additional info + List additionalInfo = info.getAdditionalInfo(); + if (!additionalInfo.isEmpty()) { + currentY += LINE_SPACING; + for (String line : additionalInfo) { + drawText(textX, currentY, line, INFO_COLOR); + currentY += ClientProxy.Font.height() + LINE_SPACING; + } + } + + // Re-enable scissor test + // GL11.glEnable(GL11.GL_SCISSOR_TEST); + } + + /** + * Draw an icon indicator. + */ + private static void drawIcon(int x, int y, String icon) { + int bgColor; + switch (icon) { + case "C": bgColor = ICON_CLASS_BG; break; + case "I": bgColor = ICON_INTERFACE_BG; break; + case "E": bgColor = ICON_ENUM_BG; break; + case "m": bgColor = ICON_METHOD_BG; break; + case "f": bgColor = ICON_FIELD_BG; break; + case "v": bgColor = ICON_VAR_BG; break; + case "p": bgColor = ICON_PARAM_BG; break; + default: bgColor = ICON_UNKNOWN_BG; break; + } + + // Draw icon background (rounded effect with small rect) + int bgY = y-2; + Gui.drawRect(x, bgY, x + ICON_SIZE, bgY + ICON_SIZE, 0xFF000000 | bgColor); + + // Draw icon letter centered + int textWidth = ClientProxy.Font.width(icon); + int textX = x + (ICON_SIZE - textWidth) / 2; + int textY = y + (ICON_SIZE - ClientProxy.Font.height()) / 2; + drawText(textX, textY, icon, 0xFFFFFFFF); + } + + /** + * Draw text using the Minecraft font renderer. + */ + private static void drawText(int x, int y, String text, int color) { + ClientProxy.Font.drawString(text, x, y, color); + } + + // ==================== DIMENSION CALCULATION ==================== + + /** + * Calculate the width needed for the content. + */ + private static int calculateContentWidth(TokenHoverInfo info) { + int maxWidth = 0; + + // Package name width + String packageName = info.getPackageName(); + if (packageName != null && !packageName.isEmpty()) { + maxWidth = Math.max(maxWidth, ClientProxy.Font.width("\u25CB " + packageName)); + } + + // Declaration width + List declaration = info.getDeclaration(); + int declWidth = 0; + for (TokenHoverInfo.TextSegment segment : declaration) { + declWidth += ClientProxy.Font.width(segment.text); + } + maxWidth = Math.max(maxWidth, declWidth); + + // Error widths + for (String error : info.getErrors()) { + maxWidth = Math.max(maxWidth, ClientProxy.Font.width(error)); + } + + // Documentation widths + for (String doc : info.getDocumentation()) { + maxWidth = Math.max(maxWidth, ClientProxy.Font.width(doc)); + } + + // Additional info widths + for (String line : info.getAdditionalInfo()) { + maxWidth = Math.max(maxWidth, ClientProxy.Font.width(line)); + } + + return maxWidth; + } + + /** + * Calculate the height needed for the content. + */ + private static int calculateContentHeight(TokenHoverInfo info) { + int lineHeight = ClientProxy.Font.height(); + int totalHeight = 0; + int lineCount = 0; + + // Errors + if (!info.getErrors().isEmpty()) { + lineCount += info.getErrors().size(); + totalHeight += LINE_SPACING; // Extra space after errors + } + + // Package name + if (info.getPackageName() != null && !info.getPackageName().isEmpty()) { + lineCount++; + } + + // Declaration + if (!info.getDeclaration().isEmpty()) { + lineCount++; + } + + // Documentation + if (!info.getDocumentation().isEmpty()) { + lineCount += info.getDocumentation().size(); + totalHeight += LINE_SPACING; // Extra space before docs + } + + // Additional info + if (!info.getAdditionalInfo().isEmpty()) { + lineCount += info.getAdditionalInfo().size(); + totalHeight += LINE_SPACING; // Extra space before info + } + + totalHeight += lineCount * (lineHeight + LINE_SPACING); + + return Math.max(ICON_SIZE, totalHeight); + } +} From 9d5c577cf4ace61c8bdea91fa6141501e40b6fed Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 01:35:32 +0200 Subject: [PATCH 023/337] Improved token info colors --- .../interpreter/hover/TokenHoverInfo.java | 146 ++++++++++-------- 1 file changed, 84 insertions(+), 62 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index b624c4cf5..5b5d06b41 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -206,36 +206,30 @@ private void extractClassInfo(Token token) { int mods = clazz.getModifiers(); // Modifiers - if (Modifier.isPublic(mods)) addSegment("public ", TextSegment.COLOR_KEYWORD); - if (Modifier.isAbstract(mods) && !clazz.isInterface()) addSegment("abstract ", TextSegment.COLOR_KEYWORD); - if (Modifier.isFinal(mods)) addSegment("final ", TextSegment.COLOR_KEYWORD); + if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isAbstract(mods) && !clazz.isInterface()) addSegment("abstract ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isFinal(mods)) addSegment("final ", TokenType.MODIFIER.getHexColor()); // Class type keyword if (clazz.isInterface()) { - addSegment("interface ", TextSegment.COLOR_KEYWORD); + addSegment("interface ", TokenType.MODIFIER.getHexColor()); } else if (clazz.isEnum()) { - addSegment("enum ", TextSegment.COLOR_KEYWORD); + addSegment("enum ", TokenType.MODIFIER.getHexColor()); } else { - addSegment("class ", TextSegment.COLOR_KEYWORD); + addSegment("class ", TokenType.MODIFIER.getHexColor()); } - // Class name - addSegment(typeInfo.getSimpleName(), TextSegment.COLOR_CLASS); + // Class name - use proper color based on type + int classColor = clazz.isInterface() ? TokenType.INTERFACE_DECL.getHexColor() + : clazz.isEnum() ? TokenType.ENUM_DECL.getHexColor() + : TokenType.IMPORTED_CLASS.getHexColor(); + addSegment(typeInfo.getSimpleName(), classColor); // Extends Class superclass = clazz.getSuperclass(); if (superclass != null && superclass != Object.class) { - addSegment(" extends ", TextSegment.COLOR_KEYWORD); - int superClassCol = TextSegment.COLOR_TYPE; - if (superclass.isInterface()) { - superClassCol = TokenType.INTERFACE_DECL.getHexColor(); - } else if (superclass.isEnum()) { - superClassCol = TokenType.ENUM_DECL.getHexColor(); - } else { - superClassCol = TokenType.IMPORTED_CLASS.getHexColor(); - - } - addSegment(superclass.getSimpleName(), superClassCol); + addSegment(" extends ", TokenType.MODIFIER.getHexColor()); + addSegment(superclass.getSimpleName(), getColorForClass(superclass)); } List declaration = null; @@ -243,19 +237,19 @@ private void extractClassInfo(Token token) { // Implements Class[] interfaces = clazz.getInterfaces(); if (interfaces.length > 0) { - addSegment(clazz.isInterface() ? " extends " : " implements ", TextSegment.COLOR_KEYWORD); + addSegment(clazz.isInterface() ? " extends " : " implements ", TokenType.MODIFIER.getHexColor()); for (int i = 0; i < Math.min(interfaces.length, 3); i++) { - if (i > 0) addSegment(", ", TextSegment.COLOR_DEFAULT); + if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); addSegment(interfaces[i].getSimpleName(), TokenType.INTERFACE_DECL.getHexColor()); } if (interfaces.length > 3) { - addSegment(", ...", TextSegment.COLOR_DEFAULT); + addSegment(", ...", TokenType.DEFAULT.getHexColor()); } } } else { // Unresolved type iconIndicator = "?"; - addSegment(typeInfo.getSimpleName(), TextSegment.COLOR_CLASS); + addSegment(typeInfo.getSimpleName(), TokenType.IMPORTED_CLASS.getHexColor()); if (!typeInfo.isResolved()) { errors.add("Cannot resolve class '" + typeInfo.getSimpleName() + "'"); } @@ -332,13 +326,14 @@ private void extractGlobalFieldInfo(Token token) { packageName = pkg; } - // Type - addSegment(declaredType.getSimpleName(), TextSegment.COLOR_TYPE); - addSegment(" ", TextSegment.COLOR_DEFAULT); + // Type - check for actual type color + int typeColor = getColorForTypeInfo(declaredType); + addSegment(declaredType.getSimpleName(), typeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); } // Field name - addSegment(fieldInfo.getName(), TextSegment.COLOR_FIELD); + addSegment(fieldInfo.getName(), TokenType.GLOBAL_FIELD.getHexColor()); } private void extractLocalFieldInfo(Token token) { @@ -349,11 +344,12 @@ private void extractLocalFieldInfo(Token token) { TypeInfo declaredType = fieldInfo.getDeclaredType(); if (declaredType != null) { - addSegment(declaredType.getSimpleName(), TextSegment.COLOR_TYPE); - addSegment(" ", TextSegment.COLOR_DEFAULT); + int typeColor = getColorForTypeInfo(declaredType); + addSegment(declaredType.getSimpleName(), typeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); } - addSegment(fieldInfo.getName(), TextSegment.COLOR_FIELD); + addSegment(fieldInfo.getName(), TokenType.LOCAL_FIELD.getHexColor()); // Show it's a local variable additionalInfo.add("Local variable"); @@ -367,18 +363,19 @@ private void extractParameterInfo(Token token) { TypeInfo declaredType = fieldInfo.getDeclaredType(); if (declaredType != null) { - addSegment(declaredType.getSimpleName(), TextSegment.COLOR_TYPE); - addSegment(" ", TextSegment.COLOR_DEFAULT); + int typeColor = getColorForTypeInfo(declaredType); + addSegment(declaredType.getSimpleName(), typeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); } - addSegment(fieldInfo.getName(), TextSegment.COLOR_PARAM); + addSegment(fieldInfo.getName(), TokenType.PARAMETER.getHexColor()); additionalInfo.add("Parameter"); } private void extractUndefinedInfo(Token token) { iconIndicator = "?"; - addSegment(token.getText(), TextSegment.COLOR_ERROR); + addSegment(token.getText(), TokenType.UNDEFINED_VAR.getHexColor()); errors.add("Cannot resolve symbol '" + token.getText() + "'"); } @@ -425,70 +422,74 @@ private void buildMethodDeclaration(Method method, TypeInfo containingType) { // Skip for now - could add later // Modifiers - if (Modifier.isPublic(mods)) addSegment("public ", TextSegment.COLOR_KEYWORD); - else if (Modifier.isProtected(mods)) addSegment("protected ", TextSegment.COLOR_KEYWORD); - else if (Modifier.isPrivate(mods)) addSegment("private ", TextSegment.COLOR_KEYWORD); + if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor()); + else if (Modifier.isProtected(mods)) addSegment("protected ", TokenType.MODIFIER.getHexColor()); + else if (Modifier.isPrivate(mods)) addSegment("private ", TokenType.MODIFIER.getHexColor()); - if (Modifier.isStatic(mods)) addSegment("static ", TextSegment.COLOR_KEYWORD); - if (Modifier.isFinal(mods)) addSegment("final ", TextSegment.COLOR_KEYWORD); - if (Modifier.isAbstract(mods)) addSegment("abstract ", TextSegment.COLOR_KEYWORD); - if (Modifier.isSynchronized(mods)) addSegment("synchronized ", TextSegment.COLOR_KEYWORD); + if (Modifier.isStatic(mods)) addSegment("static ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isFinal(mods)) addSegment("final ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isAbstract(mods)) addSegment("abstract ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isSynchronized(mods)) addSegment("synchronized ", TokenType.MODIFIER.getHexColor()); - // Return type + // Return type - check for actual type color Class returnType = method.getReturnType(); - addSegment(returnType.getSimpleName(), TextSegment.COLOR_TYPE); - addSegment(" ", TextSegment.COLOR_DEFAULT); + int returnTypeColor = getColorForClass(returnType); + addSegment(returnType.getSimpleName(), returnTypeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); // Method name - addSegment(method.getName(), TextSegment.COLOR_METHOD); + addSegment(method.getName(), TokenType.METHOD_CALL.getHexColor()); // Parameters - addSegment("(", TextSegment.COLOR_DEFAULT); + addSegment("(", TokenType.DEFAULT.getHexColor()); Class[] paramTypes = method.getParameterTypes(); java.lang.reflect.Parameter[] params = method.getParameters(); for (int i = 0; i < paramTypes.length; i++) { - if (i > 0) addSegment(", ", TextSegment.COLOR_DEFAULT); - addSegment(paramTypes[i].getSimpleName(), TextSegment.COLOR_TYPE); - addSegment(" ", TextSegment.COLOR_DEFAULT); + if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); + int paramTypeColor = getColorForClass(paramTypes[i]); + addSegment(paramTypes[i].getSimpleName(), paramTypeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); // Try to get parameter name if available String paramName = params.length > i ? params[i].getName() : "arg" + i; - addSegment(paramName, TextSegment.COLOR_PARAM); + addSegment(paramName, TokenType.PARAMETER.getHexColor()); } - addSegment(")", TextSegment.COLOR_DEFAULT); + addSegment(")", TokenType.DEFAULT.getHexColor()); } private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo containingType) { // Modifiers if (methodInfo.isStatic()) { - addSegment("static ", TextSegment.COLOR_KEYWORD); + addSegment("static ", TokenType.MODIFIER.getHexColor()); } // Return type TypeInfo returnType = methodInfo.getReturnType(); if (returnType != null) { - addSegment(returnType.getSimpleName(), TextSegment.COLOR_TYPE); - addSegment(" ", TextSegment.COLOR_DEFAULT); + int returnTypeColor = getColorForTypeInfo(returnType); + addSegment(returnType.getSimpleName(), returnTypeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); } else { - addSegment("void ", TextSegment.COLOR_KEYWORD); + addSegment("void ", TokenType.KEYWORD.getHexColor()); } // Method name - addSegment(methodInfo.getName(), TextSegment.COLOR_METHOD); + addSegment(methodInfo.getName(), TokenType.METHOD_CALL.getHexColor()); // Parameters - addSegment("(", TextSegment.COLOR_DEFAULT); + addSegment("(", TokenType.DEFAULT.getHexColor()); List params = methodInfo.getParameters(); for (int i = 0; i < params.size(); i++) { - if (i > 0) addSegment(", ", TextSegment.COLOR_DEFAULT); + if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); FieldInfo param = params.get(i); TypeInfo paramType = param.getDeclaredType(); if (paramType != null) { - addSegment(paramType.getSimpleName(), TextSegment.COLOR_TYPE); - addSegment(" ", TextSegment.COLOR_DEFAULT); + int paramTypeColor = getColorForTypeInfo(paramType); + addSegment(paramType.getSimpleName(), paramTypeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); } - addSegment(param.getName(), TextSegment.COLOR_PARAM); + addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); } - addSegment(")", TextSegment.COLOR_DEFAULT); + addSegment(")", TokenType.DEFAULT.getHexColor()); } private void extractJavadoc(Method method) { @@ -502,6 +503,27 @@ private void extractJavadoc(Method method) { } } + /** + * Get the appropriate color for a Class based on its type. + */ + private int getColorForClass(Class clazz) { + if (clazz.isInterface()) return TokenType.INTERFACE_DECL.getHexColor(); + if (clazz.isEnum()) return TokenType.ENUM_DECL.getHexColor(); + return TokenType.IMPORTED_CLASS.getHexColor(); + } + + /** + * Get the appropriate color for a TypeInfo based on its Java class. + */ + private int getColorForTypeInfo(TypeInfo typeInfo) { + if (typeInfo != null) { + Class clazz = typeInfo.getJavaClass(); + if (clazz != null) + return getColorForClass(clazz); + } + return TokenType.IMPORTED_CLASS.getHexColor(); + } + // ==================== GETTERS ==================== public String getPackageName() { return packageName; } From 741608a353dd5b948f48efc0e312796000150073 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 17:28:28 +0200 Subject: [PATCH 024/337] Click to pin HoverState option --- .../client/gui/util/GuiScriptTextArea.java | 30 +++++++++- .../script/interpreter/hover/HoverState.java | 59 ++++++++++++++++++- 2 files changed, 86 insertions(+), 3 deletions(-) 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 955827613..d80b75eb9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -86,6 +86,8 @@ private int getPaddedLineCount() { private final ScrollState scroll = new ScrollState(); private final SelectionState selection = new SelectionState(); private final HoverState hoverState = new HoverState(); + /** When true, clicking a token will pin its hover tooltip until dismissed. */ + public boolean clickToPinEnabled = false; // ==================== UI COMPONENTS ==================== private int cursorCounter; @@ -135,6 +137,8 @@ public void init(int x, int y, int width, int height, String text) { KEYS_OVERLAY.openOnClick = true; initGui(); initializeKeyBindings(); + // Propagate click-to-pin option into hover state + hoverState.setClickToPinEnabled(clickToPinEnabled); } public void initGui() { int endX = x + width, endY = y + height; @@ -993,7 +997,8 @@ private Object[] getTokenAtScreenPosition(int xMouse, int yMouse) { int accumulatedWidth = 0; for (int i = 0; i < lineText.length(); i++) { int charWidth = ClientProxy.Font.width(String.valueOf(lineText.charAt(i))); - if (relativeX < accumulatedWidth + charWidth) { + // Check if mouse is in first half or second half of character + if (relativeX < accumulatedWidth + charWidth / 2) { charPos = i; break; } @@ -2195,6 +2200,29 @@ public void mouseClicked(int xMouse, int yMouse, int mouseButton) { this.lastClicked = time; activeTextfield = this; + + // Click-to-pin handling: if enabled, clicking a token will pin/unpin its tooltip + if (mouseButton == 0 && hoverState.isClickToPinEnabled()) { + Object[] tokenInfo = getTokenAtScreenPosition(xMouse, yMouse); + if (tokenInfo != null) { + Token clickedToken = (Token) tokenInfo[0]; + int tokenScreenX = (Integer) tokenInfo[1]; + int tokenScreenY = (Integer) tokenInfo[2]; + int tokenWidth = (Integer) tokenInfo[3]; + + // If already pinned on same token, unpin; otherwise pin this token + if (hoverState.isPinned() && hoverState.getHoveredToken() == clickedToken) { + hoverState.unpin(); + } else { + hoverState.pinToken(clickedToken, tokenScreenX, tokenScreenY, tokenWidth); + } + } else { + // Clicked outside any token -> unpin any pinned tooltip + if (hoverState.isPinned()) { + hoverState.unpin(); + } + } + } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java index 05f403432..d1d4435ba 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java @@ -43,6 +43,13 @@ public class HoverState { private int tokenScreenX; private int tokenScreenY; private int tokenWidth; + + /** Pinned token (set when user clicks a token to keep tooltip open) */ + private Token pinnedToken; + private TokenHoverInfo pinnedHoverInfo; + + /** Whether click-to-pin behaviour is enabled for this hover state. */ + private boolean clickToPinEnabled = true; // ==================== UPDATE ==================== @@ -144,6 +151,13 @@ public void update(int mouseX, int mouseY, ScriptDocument document, public void update(int mouseX, int mouseY, Token token, int tokenX, int tokenY, int tokenW) { lastMouseX = mouseX; lastMouseY = mouseY; + // If a token has been pinned by click, ignore mouse movement updates + if (pinnedToken != null) { + // keep pinned tooltip visible + tooltipVisible = true; + hoverInfo = pinnedHoverInfo; + return; + } if (token == null) { clearHover(); @@ -179,8 +193,11 @@ public void clearHover() { if (hoveredToken != null) { hoveredToken = null; hoverStartTime = 0; - tooltipVisible = false; - hoverInfo = null; + // Do not clear tooltipInfo here if pinned; if not pinned, hide tooltip. + if (pinnedToken == null) { + tooltipVisible = false; + hoverInfo = null; + } } } @@ -191,6 +208,44 @@ public void hideTooltip() { tooltipVisible = false; } + /** + * Enable or disable click-to-pin behaviour. + */ + public void setClickToPinEnabled(boolean enabled) { + this.clickToPinEnabled = enabled; + } + + public boolean isClickToPinEnabled() { + return clickToPinEnabled; + } + + /** + * Pin a token so its tooltip stays visible until explicitly unpinned. + */ + public void pinToken(Token token, int tokenX, int tokenY, int tokenW) { + if (token == null) return; + this.pinnedToken = token; + this.pinnedHoverInfo = TokenHoverInfo.fromToken(token); + this.tooltipVisible = pinnedHoverInfo != null && pinnedHoverInfo.hasContent(); + this.tokenScreenX = tokenX; + this.tokenScreenY = tokenY; + this.tokenWidth = tokenW; + // ensure hoveredToken reflects pinned token + this.hoveredToken = token; + } + + /** + * Unpin any pinned token and hide tooltip. + */ + public void unpin() { + this.pinnedToken = null; + this.pinnedHoverInfo = null; + this.tooltipVisible = false; + this.hoverInfo = null; + } + + public boolean isPinned() { return pinnedToken != null; } + // ==================== POSITION CALCULATION ==================== /** From 351e663212d6dc25b8d61756c39d7d4e99427202 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 17:29:36 +0200 Subject: [PATCH 025/337] Improved TokenHoverRenderer --- .../client/gui/util/GuiScriptTextArea.java | 9 +- .../interpreter/hover/TokenHoverRenderer.java | 451 ++++++++++++------ 2 files changed, 303 insertions(+), 157 deletions(-) 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 d80b75eb9..6616a2dd1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -480,6 +480,7 @@ public void drawTextBox(int xMouse, int yMouse) { // Draw line number gutter background + int viewportX = x + LINE_NUMBER_GUTTER_WIDTH; drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000); // Draw text viewport background (starts after gutter) drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000); @@ -886,9 +887,11 @@ public void drawTextBox(int xMouse, int yMouse) { // Draw hover tooltip (on top of everything) if (hoverState.isTooltipVisible()) { - ScaledResolution sr = new ScaledResolution(Minecraft.getMinecraft(), - Minecraft.getMinecraft().displayWidth, Minecraft.getMinecraft().displayHeight); - TokenHoverRenderer.render(hoverState, sr.getScaledWidth(), sr.getScaledHeight()); + int xOffset = hasVerticalScrollbar() ? -8 : -2; + int viewportWidth = width - LINE_NUMBER_GUTTER_WIDTH; + int viewportY = y; + int viewportHeight = height; + TokenHoverRenderer.render(hoverState, viewportX, viewportWidth+xOffset, viewportY, viewportHeight); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index 08181ff6b..aa0107472 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -2,10 +2,10 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Gui; -import net.minecraft.client.gui.ScaledResolution; import noppes.npcs.client.ClientProxy; import org.lwjgl.opengl.GL11; +import java.util.ArrayList; import java.util.List; /** @@ -18,7 +18,7 @@ * - Documentation/Javadoc * - Error messages in red * - * Handles positioning to keep tooltip on screen. + * Uses a unified approach for width/height calculation based on max available width. */ public class TokenHoverRenderer { @@ -27,23 +27,17 @@ public class TokenHoverRenderer { /** Padding inside the tooltip box */ private static final int PADDING = 6; - /** Gap between icon and text */ - private static final int ICON_GAP = 4; - - /** Icon box size */ - private static final int ICON_SIZE = 12; - - /** Line spacing */ + /** Line spacing between rows */ private static final int LINE_SPACING = 2; /** Vertical offset from token */ private static final int VERTICAL_OFFSET = 4; - /** Maximum tooltip width */ - private static final int MAX_WIDTH = 400; + /** Maximum tooltip width as percentage of viewport */ + private static final float MAX_WIDTH_RATIO = 0.9f; /** Minimum tooltip width */ - private static final int MIN_WIDTH = 150; + private static final int MIN_WIDTH = 50; // ==================== COLORS ==================== @@ -61,143 +55,147 @@ public class TokenHoverRenderer { /** Info text color */ private static final int INFO_COLOR = 0xFF808080; - - // ==================== ICON COLORS ==================== - private static final int ICON_CLASS_BG = 0xFF4A6B8A; // Blue for classes - private static final int ICON_INTERFACE_BG = 0xFF8A6B4A; // Orange-brown for interfaces - private static final int ICON_ENUM_BG = 0xFF6B8A4A; // Green for enums - private static final int ICON_METHOD_BG = 0xFF8A4A6B; // Purple for methods - private static final int ICON_FIELD_BG = 0xFF4A8A6B; // Teal for fields - private static final int ICON_VAR_BG = 0xFF6B6B8A; // Blue-gray for variables - private static final int ICON_PARAM_BG = 0xFF8A8A4A; // Yellow for parameters - private static final int ICON_UNKNOWN_BG = 0xFF8A4A4A; // Red for unknown + /** Documentation text color */ + private static final int DOC_COLOR = 0xFFA9B7C6; // ==================== RENDERING ==================== /** * Render the hover tooltip. - * - * @param hoverState The current hover state - * @param screenWidth Screen width for positioning - * @param screenHeight Screen height for positioning */ - public static void render(HoverState hoverState, int screenWidth, int screenHeight) { + public static void render(HoverState hoverState, int viewportX, int viewportWidth, int viewportY, int viewportHeight) { if (!hoverState.isTooltipVisible()) return; TokenHoverInfo info = hoverState.getHoverInfo(); if (info == null || !info.hasContent()) return; - // Calculate content dimensions - int contentWidth = calculateContentWidth(info); - int contentHeight = calculateContentHeight(info); + int lineHeight = ClientProxy.Font.height(); + int tokenX = hoverState.getTokenScreenX(); + int tokenY = hoverState.getTokenScreenY(); + + // Calculate max available width for content + int maxContentWidth = getMaxContentWidth(viewportX, viewportWidth, tokenX); - // Add padding - int boxWidth = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, contentWidth + PADDING * 2 + ICON_SIZE + ICON_GAP)); + // Calculate actual content width needed (clamped to max) + int contentWidth = calculateContentWidth(info, maxContentWidth); + + // Calculate height based on the actual content width (accounts for wrapping) + int contentHeight = calculateContentHeight(info, contentWidth); + + // Box dimensions + int boxWidth = contentWidth + PADDING * 2; int boxHeight = contentHeight + PADDING * 2; // Position the tooltip - int tokenX = hoverState.getTokenScreenX(); - int tokenY = hoverState.getTokenScreenY(); - int lineHeight = ClientProxy.Font.height(); - - // Default: show below the token int tooltipX = tokenX; int tooltipY = tokenY + lineHeight + VERTICAL_OFFSET; - // Adjust if tooltip would go off-screen - if (tooltipX + boxWidth > screenWidth - 10) { - tooltipX = screenWidth - boxWidth - 10; + // Clamp X position to viewport + int rightBound = viewportX + viewportWidth; + if (tooltipX + boxWidth > rightBound) { + tooltipX = rightBound - boxWidth; } - if (tooltipX < 10) { - tooltipX = 10; + if (tooltipX < viewportX) { + tooltipX = viewportX; } - if (tooltipY + boxHeight > screenHeight - 10) { - // Show above the token instead + // If box still doesn't fit horizontally, shrink it + int availableWidth = rightBound - tooltipX; + if (boxWidth > availableWidth) { + boxWidth = availableWidth; + } + + // Clamp Y position to viewport + int bottomBound = viewportY + viewportHeight; + if (tooltipY + boxHeight > bottomBound) { + // Try rendering above the token tooltipY = tokenY - boxHeight - VERTICAL_OFFSET; } - if (tooltipY < 10) { - tooltipY = 10; + // If still doesn't fit above, clamp to viewport top + if (tooltipY < viewportY) { + tooltipY = viewportY; } - // Render the tooltip - renderTooltipBox(tooltipX, tooltipY, boxWidth, boxHeight, info); + // Render the tooltip - use maxContentWidth for consistent wrapping + renderTooltipBox(tooltipX, tooltipY, boxWidth, maxContentWidth, info); + } + + /** + * Calculate the maximum content width based on viewport and token position. + */ + private static int getMaxContentWidth(int viewportX, int viewportWidth, int tokenX) { + // Max width is based on viewport size (80% by default) + // This scales naturally with viewport size - no artificial hard cap + int maxWidth = (int)(viewportWidth * MAX_WIDTH_RATIO); + + // The tooltip can expand across the entire viewport width + // (positioning logic will shift it left if needed) + return Math.max(MIN_WIDTH, maxWidth); } /** * Render the tooltip box with all content. */ - private static void renderTooltipBox(int x, int y, int width, int height, TokenHoverInfo info) { - // Disable scissor test temporarily for tooltip rendering + private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, TokenHoverInfo info) { GL11.glDisable(GL11.GL_SCISSOR_TEST); + int boxHeight = calculateContentHeight(info, wrapWidth) + PADDING * 1; + // Draw background - Gui.drawRect(x, y, x + width, y + height, BG_COLOR); + Gui.drawRect(x, y, x + boxWidth, y + boxHeight, BG_COLOR); // Draw border - Gui.drawRect(x, y, x + width, y + 1, BORDER_COLOR); - Gui.drawRect(x, y + height - 1, x + width, y + height, BORDER_COLOR); - Gui.drawRect(x, y, x + 1, y + height, BORDER_COLOR); - Gui.drawRect(x + width - 1, y, x + width, y + height, BORDER_COLOR); - - int contentX = x + PADDING; - int contentY = y + PADDING; - int textX = contentX + ICON_SIZE + ICON_GAP; - int maxTextWidth = width - PADDING * 2 - ICON_SIZE - ICON_GAP; - - // Draw icon - String icon = info.getIconIndicator(); - if (icon != null && !icon.isEmpty()) { - drawIcon(contentX, contentY, icon); - } + Gui.drawRect(x, y, x + boxWidth, y + 1, BORDER_COLOR); + Gui.drawRect(x, y + boxHeight - 1, x + boxWidth, y + boxHeight, BORDER_COLOR); + Gui.drawRect(x, y, x + 1, y + boxHeight, BORDER_COLOR); + Gui.drawRect(x + boxWidth - 1, y, x + boxWidth, y + boxHeight, BORDER_COLOR); - int currentY = contentY; + int textX = x + PADDING; + int currentY = y + PADDING; + int lineHeight = ClientProxy.Font.height(); - // Draw errors first (if any) + // Draw errors first List errors = info.getErrors(); if (!errors.isEmpty()) { for (String error : errors) { - drawText(textX, currentY, error, ERROR_COLOR); - currentY += ClientProxy.Font.height() + LINE_SPACING; + List wrappedLines = wrapText(error, wrapWidth); + for (String line : wrappedLines) { + drawText(textX, currentY, line, ERROR_COLOR); + currentY += lineHeight + LINE_SPACING; + } } - currentY += LINE_SPACING; // Extra space after errors + currentY += LINE_SPACING; } // Draw package name String packageName = info.getPackageName(); if (packageName != null && !packageName.isEmpty()) { - // Draw package icon - drawText(textX, currentY, "\u25CB " + packageName, PACKAGE_COLOR); - currentY += ClientProxy.Font.height() + LINE_SPACING; + String packageText = "\u25CB " + packageName; + List wrappedLines = wrapText(packageText, wrapWidth); + for (String line : wrappedLines) { + drawText(textX, currentY, line, PACKAGE_COLOR); + currentY += lineHeight + LINE_SPACING; + } } - // Draw declaration + // Draw declaration (colored segments with wrapping) List declaration = info.getDeclaration(); if (!declaration.isEmpty()) { - int segmentX = textX; - for (TokenHoverInfo.TextSegment segment : declaration) { - int segmentWidth = ClientProxy.Font.width(segment.text); - - // Check if we need to wrap (simple wrapping for now) - if (segmentX + segmentWidth > x + width - PADDING && segmentX > textX) { - currentY += ClientProxy.Font.height() + LINE_SPACING; - segmentX = textX; - } - - drawText(segmentX, currentY, segment.text, 0xFF000000 | segment.color); - segmentX += segmentWidth; - } - currentY += ClientProxy.Font.height() + LINE_SPACING; + currentY = drawWrappedSegments(textX, currentY, wrapWidth, declaration); + currentY += LINE_SPACING; } // Draw documentation List docs = info.getDocumentation(); if (!docs.isEmpty()) { - currentY += LINE_SPACING; // Extra space before docs + currentY += LINE_SPACING; for (String doc : docs) { - drawText(textX, currentY, doc, 0xFFA9B7C6); - currentY += ClientProxy.Font.height() + LINE_SPACING; + List wrappedLines = wrapText(doc, wrapWidth); + for (String line : wrappedLines) { + drawText(textX, currentY, line, DOC_COLOR); + currentY += lineHeight + LINE_SPACING; + } } } @@ -205,128 +203,273 @@ private static void renderTooltipBox(int x, int y, int width, int height, TokenH List additionalInfo = info.getAdditionalInfo(); if (!additionalInfo.isEmpty()) { currentY += LINE_SPACING; - for (String line : additionalInfo) { - drawText(textX, currentY, line, INFO_COLOR); - currentY += ClientProxy.Font.height() + LINE_SPACING; + for (String infoLine : additionalInfo) { + List wrappedLines = wrapText(infoLine, wrapWidth); + for (String line : wrappedLines) { + drawText(textX, currentY, line, INFO_COLOR); + currentY += lineHeight + LINE_SPACING; + } + } + } + } + + /** + * Draw colored text segments with word wrapping. + * Returns the Y position after drawing. + */ + private static int drawWrappedSegments(int startX, int startY, int maxWidth, List segments) { + int lineHeight = ClientProxy.Font.height(); + int currentX = startX; + int currentY = startY; + + for (TokenHoverInfo.TextSegment segment : segments) { + String text = segment.text; + int color = 0xFF000000 | segment.color; + + // Split segment into words for wrapping + String[] words = text.split("(?<=\\s)|(?=\\s)"); // Keep whitespace as separate tokens + + for (String word : words) { + int wordWidth = ClientProxy.Font.width(word); + + // Check if word fits on current line + if (currentX + wordWidth > startX + maxWidth && currentX > startX) { + // Wrap to next line + currentY += lineHeight + LINE_SPACING; + currentX = startX; + + // Skip leading whitespace on new line + if (word.trim().isEmpty()) { + continue; + } + } + + drawText(currentX, currentY, word, color); + currentX += wordWidth; } } - // Re-enable scissor test - // GL11.glEnable(GL11.GL_SCISSOR_TEST); + return currentY + lineHeight; } /** - * Draw an icon indicator. + * Wrap text to fit within maxWidth. + * Returns list of lines. */ - private static void drawIcon(int x, int y, String icon) { - int bgColor; - switch (icon) { - case "C": bgColor = ICON_CLASS_BG; break; - case "I": bgColor = ICON_INTERFACE_BG; break; - case "E": bgColor = ICON_ENUM_BG; break; - case "m": bgColor = ICON_METHOD_BG; break; - case "f": bgColor = ICON_FIELD_BG; break; - case "v": bgColor = ICON_VAR_BG; break; - case "p": bgColor = ICON_PARAM_BG; break; - default: bgColor = ICON_UNKNOWN_BG; break; + private static List wrapText(String text, int maxWidth) { + List lines = new ArrayList<>(); + + if (text == null || text.isEmpty()) { + return lines; + } + + // Use Minecraft's built-in wrapping if available + Minecraft mc = Minecraft.getMinecraft(); + if (mc.fontRenderer != null) { + @SuppressWarnings("unchecked") + List wrapped = mc.fontRenderer.listFormattedStringToWidth(text, maxWidth); + return wrapped; } - // Draw icon background (rounded effect with small rect) - int bgY = y-2; - Gui.drawRect(x, bgY, x + ICON_SIZE, bgY + ICON_SIZE, 0xFF000000 | bgColor); + // Fallback: simple word wrapping + String[] words = text.split(" "); + StringBuilder currentLine = new StringBuilder(); + + for (String word : words) { + String testLine = currentLine.length() == 0 ? word : currentLine + " " + word; + int testWidth = ClientProxy.Font.width(testLine); + + if (testWidth > maxWidth && currentLine.length() > 0) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(word); + } else { + if (currentLine.length() > 0) { + currentLine.append(" "); + } + currentLine.append(word); + } + } - // Draw icon letter centered - int textWidth = ClientProxy.Font.width(icon); - int textX = x + (ICON_SIZE - textWidth) / 2; - int textY = y + (ICON_SIZE - ClientProxy.Font.height()) / 2; - drawText(textX, textY, icon, 0xFFFFFFFF); + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + } + + if (lines.isEmpty()) { + lines.add(text); + } + return lines; } /** - * Draw text using the Minecraft font renderer. + * Draw text using the font renderer. */ private static void drawText(int x, int y, String text, int color) { - ClientProxy.Font.drawString(text, x, y, color); + ClientProxy.Font.drawString(text, x, y, color); } // ==================== DIMENSION CALCULATION ==================== /** - * Calculate the width needed for the content. + * Calculate the width needed for the content, clamped to maxWidth. + * Returns the width of the longest wrapped line. */ - private static int calculateContentWidth(TokenHoverInfo info) { - int maxWidth = 0; + private static int calculateContentWidth(TokenHoverInfo info, int maxWidth) { + int longestLineWidth = 0; - // Package name width + // Package name String packageName = info.getPackageName(); if (packageName != null && !packageName.isEmpty()) { - maxWidth = Math.max(maxWidth, ClientProxy.Font.width("\u25CB " + packageName)); + String packageText = "\u25CB " + packageName; + List wrappedLines = wrapText(packageText, maxWidth); + for (String line : wrappedLines) { + longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line)); + } } - // Declaration width - List declaration = info.getDeclaration(); - int declWidth = 0; - for (TokenHoverInfo.TextSegment segment : declaration) { - declWidth += ClientProxy.Font.width(segment.text); + // Declaration - wrap segments and find longest line + if (!info.getDeclaration().isEmpty()) { + int declarationLongestLine = calculateSegmentsLongestLine(maxWidth, info.getDeclaration()); + longestLineWidth = Math.max(longestLineWidth, declarationLongestLine); } - maxWidth = Math.max(maxWidth, declWidth); - // Error widths + // Errors for (String error : info.getErrors()) { - maxWidth = Math.max(maxWidth, ClientProxy.Font.width(error)); + List wrappedLines = wrapText(error, maxWidth); + for (String line : wrappedLines) { + longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line)); + } } - // Documentation widths + // Documentation for (String doc : info.getDocumentation()) { - maxWidth = Math.max(maxWidth, ClientProxy.Font.width(doc)); + List wrappedLines = wrapText(doc, maxWidth); + for (String line : wrappedLines) { + longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line)); + } } - // Additional info widths + // Additional info for (String line : info.getAdditionalInfo()) { - maxWidth = Math.max(maxWidth, ClientProxy.Font.width(line)); + List wrappedLines = wrapText(line, maxWidth); + for (String wrappedLine : wrappedLines) { + longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(wrappedLine)); + } } - return maxWidth; + // Ensure minimum width + return Math.max(50, longestLineWidth); } /** - * Calculate the height needed for the content. + * Calculate the height needed for the content given the available width. + * Uses the same wrapping logic as rendering. */ - private static int calculateContentHeight(TokenHoverInfo info) { + private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) { int lineHeight = ClientProxy.Font.height(); int totalHeight = 0; - int lineCount = 0; // Errors - if (!info.getErrors().isEmpty()) { - lineCount += info.getErrors().size(); + List errors = info.getErrors(); + if (!errors.isEmpty()) { + for (String error : errors) { + List wrappedLines = wrapText(error, contentWidth); + totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING); + } totalHeight += LINE_SPACING; // Extra space after errors } // Package name - if (info.getPackageName() != null && !info.getPackageName().isEmpty()) { - lineCount++; + String packageName = info.getPackageName(); + if (packageName != null && !packageName.isEmpty()) { + String packageText = "\u25CB " + packageName; + List wrappedLines = wrapText(packageText, contentWidth); + totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING); } - // Declaration - if (!info.getDeclaration().isEmpty()) { - lineCount++; + // Declaration (colored segments with wrapping) + List declaration = info.getDeclaration(); + if (!declaration.isEmpty()) { + totalHeight += calculateSegmentsHeight(contentWidth, declaration); + totalHeight += LINE_SPACING; } // Documentation - if (!info.getDocumentation().isEmpty()) { - lineCount += info.getDocumentation().size(); + List docs = info.getDocumentation(); + if (!docs.isEmpty()) { totalHeight += LINE_SPACING; // Extra space before docs + for (String doc : docs) { + List wrappedLines = wrapText(doc, contentWidth); + totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING); + } } // Additional info - if (!info.getAdditionalInfo().isEmpty()) { - lineCount += info.getAdditionalInfo().size(); - totalHeight += LINE_SPACING; // Extra space before info + List additionalInfo = info.getAdditionalInfo(); + if (!additionalInfo.isEmpty()) { + totalHeight += LINE_SPACING; + for (String infoLine : additionalInfo) { + List wrappedLines = wrapText(infoLine, contentWidth); + totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING); + } + } + + return Math.max(lineHeight, totalHeight); + } + + /** + * Calculate height needed for wrapped segments. + */ + private static int calculateSegmentsHeight(int maxWidth, List segments) { + int lineHeight = ClientProxy.Font.height(); + int currentLineWidth = 0; + int lineCount = 1; + + for (TokenHoverInfo.TextSegment segment : segments) { + String text = segment.text; + String[] words = text.split("(?<=\\s)|(?=\\s)"); + + for (String word : words) { + int wordWidth = ClientProxy.Font.width(word); + + if (currentLineWidth + wordWidth > maxWidth && currentLineWidth > 0) { + lineCount++; + currentLineWidth = word.trim().isEmpty() ? 0 : wordWidth; + } else { + currentLineWidth += wordWidth; + } + } + } + + return lineCount * (lineHeight + LINE_SPACING); + } + + /** + * Calculate the width of the longest line when segments are wrapped. + */ + private static int calculateSegmentsLongestLine(int maxWidth, List segments) { + int currentLineWidth = 0; + int longestLineWidth = 0; + + for (TokenHoverInfo.TextSegment segment : segments) { + String text = segment.text; + String[] words = text.split("(?<=\\s)|(?=\\s)"); + + for (String word : words) { + int wordWidth = ClientProxy.Font.width(word); + + if (currentLineWidth + wordWidth > maxWidth && currentLineWidth > 0) { + // Line break - record current line width and start new line + longestLineWidth = Math.max(longestLineWidth, currentLineWidth); + currentLineWidth = word.trim().isEmpty() ? 0 : wordWidth; + } else { + currentLineWidth += wordWidth; + } + } } - totalHeight += lineCount * (lineHeight + LINE_SPACING); + // Don't forget the last line + longestLineWidth = Math.max(longestLineWidth, currentLineWidth); - return Math.max(ICON_SIZE, totalHeight); + return longestLineWidth; } } From a4692388a8b1b16dc9b1c992b52b3ff0077e3f58 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 18:12:02 +0200 Subject: [PATCH 026/337] Improved fetching token under mouse logic --- .../client/gui/util/GuiScriptTextArea.java | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) 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 6616a2dd1..474376335 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -972,15 +972,14 @@ private Object[] getTokenAtScreenPosition(int xMouse, int yMouse) { ScriptTextContainer scriptContainer = (ScriptTextContainer) container; // Check if mouse is within the text viewport - int textAreaX = x + LINE_NUMBER_GUTTER_WIDTH + 1; - if (xMouse < textAreaX || xMouse > x + width || yMouse < y || yMouse > y + height) { + int viewportX = x + LINE_NUMBER_GUTTER_WIDTH + 1; + if (xMouse < viewportX || xMouse > x + width || yMouse < y || yMouse > y + height) { return null; } // Adjust mouse position relative to text area - int relativeX = xMouse - textAreaX; int relativeY = yMouse - y; - + // Account for fractional scrolling double fracOffset = scroll.getFractionalOffset(); double fracPixels = fracOffset * container.lineHeight; @@ -992,39 +991,23 @@ private Object[] getTokenAtScreenPosition(int xMouse, int yMouse) { return null; } - LineData lineData = container.lines.get(lineIdx); - String lineText = lineData.text; - - // Find which character position in the line - int charPos = 0; - int accumulatedWidth = 0; - for (int i = 0; i < lineText.length(); i++) { - int charWidth = ClientProxy.Font.width(String.valueOf(lineText.charAt(i))); - // Check if mouse is in first half or second half of character - if (relativeX < accumulatedWidth + charWidth / 2) { - charPos = i; - break; - } - accumulatedWidth += charWidth; - charPos = i + 1; - } - - // Convert to global position - int globalPos = lineData.start + Math.min(charPos, lineText.length()); + ScriptLine lineData = container.getDocument().getLine(lineIdx); + String lineText = lineData.getText(); + int lineStart = lineData.getGlobalStart(); // Get the token at this position - Token token = scriptContainer.getInterpreterTokenAt(globalPos); - if (token == null) { + int globalMouseX = getSelectionPos(xMouse,yMouse); + Token token = scriptContainer.getInterpreterTokenAt(globalMouseX); + if (token == null) return null; - } // Calculate token's screen position - int tokenLocalStart = token.getGlobalStart() - lineData.start; - int tokenLocalEnd = token.getGlobalEnd() - lineData.start; + int tokenLocalStart = token.getGlobalStart() - lineStart; + int tokenLocalEnd = token.getGlobalEnd() - lineStart; tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, lineText.length())); tokenLocalEnd = Math.max(0, Math.min(tokenLocalEnd, lineText.length())); - int tokenScreenX = textAreaX + ClientProxy.Font.width(lineText.substring(0, tokenLocalStart)); + int tokenScreenX = viewportX + ClientProxy.Font.width(lineText.substring(0, tokenLocalStart)); int tokenScreenY = y + (lineIdx - scroll.getScrolledLine()) * container.lineHeight - (int)fracPixels; int tokenWidth = ClientProxy.Font.width(lineText.substring(tokenLocalStart, tokenLocalEnd)); From 2c645beddf2b3f88dbe01769b14bd7dec1a85d15 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 18:15:13 +0200 Subject: [PATCH 027/337] Improved hover state hover logic --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- .../client/gui/util/script/interpreter/hover/HoverState.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 474376335..028f46293 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1020,7 +1020,7 @@ private Object[] getTokenAtScreenPosition(int xMouse, int yMouse) { */ private void updateHoverState(int xMouse, int yMouse) { // Don't show tooltips when not active, clicking, or when overlays are visible - if (!active || !isEnabled() || clicked || searchBar.isVisible() || + if (!isEnabled() || clicked || searchBar.isVisible() || goToLineDialog.isVisible() || KEYS_OVERLAY.isVisible() || renameHandler.isActive()) { hoverState.clearHover(); return; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java index d1d4435ba..65e9d7c2d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java @@ -19,7 +19,7 @@ public class HoverState { /** Minimum hover time (ms) before showing tooltip */ - private static final long HOVER_DELAY_MS = 500; + private static final long HOVER_DELAY_MS = 100; // ==================== STATE ==================== @@ -151,6 +151,7 @@ public void update(int mouseX, int mouseY, ScriptDocument document, public void update(int mouseX, int mouseY, Token token, int tokenX, int tokenY, int tokenW) { lastMouseX = mouseX; lastMouseY = mouseY; + clickToPinEnabled=false; // If a token has been pinned by click, ignore mouse movement updates if (pinnedToken != null) { // keep pinned tooltip visible From 28761df3b22e589eac03712278856a80058a7564 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 18:16:33 +0200 Subject: [PATCH 028/337] oops forgor --- .../client/gui/util/script/interpreter/ScriptLine.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 1383891bd..8a66f69f0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -22,9 +22,9 @@ public class ScriptLine { private static final char COLOR_CHAR = '\u00A7'; private final String text; // The actual text of this line - private final int globalStart; // Start offset in the full document - private final int globalEnd; // End offset in the full document (exclusive) - private final int lineIndex; // 0-based line number + public final int globalStart; // Start offset in the full document + public final int globalEnd; // End offset in the full document (exclusive) + public final int lineIndex; // 0-based line number private final List tokens = new ArrayList<>(); private final List indentGuides = new ArrayList<>(); // Column positions for indent guides @@ -71,7 +71,7 @@ public Token getLastToken() { public Token getTokenAt(int globalPosition) { for (Token t : tokens) { - if (globalPosition >= t.getGlobalStart() && globalPosition < t.getGlobalEnd()) { + if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd()) { return t; } } From 586d43f08bf929bdfcc9974a599b5a045279c829 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 18:33:54 +0200 Subject: [PATCH 029/337] Fetched correct package name for Class types that have null getPackage --- .../client/gui/util/script/interpreter/TypeInfo.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java index 3967ed535..a55d8c750 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -77,7 +77,15 @@ public static TypeInfo fromClass(Class clazz) { String fullName = clazz.getName(); String simpleName = clazz.getSimpleName(); Package pkg = clazz.getPackage(); - String packageName = pkg != null ? pkg.getName() : ""; + String packageName = ""; + if (pkg != null) { + packageName = pkg.getName(); + } else if (!fullName.equals(simpleName)) { + int lastDot = fullName.lastIndexOf('.'); + if (lastDot > 0) { + packageName = fullName.substring(0, lastDot); + } + } TypeInfo enclosing = null; if (clazz.getEnclosingClass() != null) { From 3982d5082d5b6c30d7ab213ea7ed511701546fee Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 19:35:57 +0200 Subject: [PATCH 030/337] Hooked FieldAccessInfo so it's a field's default data instead of the basic FieldInfo, as access info has more context --- .../script/interpreter/ScriptDocument.java | 104 ++++++++---------- .../util/script/interpreter/ScriptLine.java | 2 + .../gui/util/script/interpreter/Token.java | 6 +- 3 files changed, 54 insertions(+), 58 deletions(-) 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 8b6c08c0c..974e25b29 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 @@ -2241,13 +2241,23 @@ private void markChainedFieldAccesses(List marks) { if (isExcluded(segPos[0])) continue; + boolean isLastSegment = (i == chainSegments.size() - 1); + boolean isStaticAccessSeg = false; + if (i - 1 >= 0) { + String prev = chainSegments.get(i - 1); + if (!prev.isEmpty() && Character.isUpperCase(prev.charAt(0))) + isStaticAccessSeg = true; + } + // Handle the first segment if we're marking it (i.e., when preceded by dot) if (i == 0 && firstIsPrecededByDot) { // This is a field access on a receiver (e.g., the "thePlayer" in "getMinecraft().thePlayer") TypeInfo receiverType = resolveReceiverChain(chainStart); if (receiverType != null && receiverType.hasField(segment)) { FieldInfo fieldInfo = receiverType.getFieldInfo(segment); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); + FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], receiverType, fieldInfo, isLastSegment, isStaticAccessSeg); + + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; } else { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); @@ -2270,26 +2280,9 @@ private void markChainedFieldAccesses(List marks) { FieldInfo fieldInfo = currentType.getFieldInfo(segment); // Check if this is the last segment and if it's in an assignment context - boolean isLastSegment = (i == chainSegments.size() - 1); - if (isLastSegment) { - // Create FieldAccessInfo for validation - boolean isStaticAccess = Character.isUpperCase(chainSegments.get(i-1).charAt(0)); - FieldAccessInfo accessInfo = new FieldAccessInfo( - segment, segPos[0], segPos[1], currentType, fieldInfo, isStaticAccess - ); - - // Set expected type from assignment context - TypeInfo expectedType = findExpectedTypeAtPosition(segPos[0]); - if (expectedType != null) { - accessInfo.setExpectedType(expectedType); - } - - accessInfo.validate(); - fieldAccesses.add(accessInfo); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - } else { - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); - } + // Create and mark FieldAccessInfo for this segment (last or intermediate) + FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], currentType, fieldInfo, isLastSegment, isStaticAccessSeg); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); // Update currentType for next segment currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; @@ -2368,25 +2361,14 @@ private void markChainedFieldAccesses(List marks) { hasMoreSegments = true; } - if (!hasMoreSegments) { - // This is the final field - create FieldAccessInfo for validation - boolean isStaticAccess = false; // Determine based on receiver - FieldAccessInfo accessInfo = new FieldAccessInfo( - firstField, identStart, identEnd, receiverType, fInfo, isStaticAccess - ); - - // Set expected type from assignment context - TypeInfo expectedType = findExpectedTypeAtPosition(identStart); - if (expectedType != null) { - accessInfo.setExpectedType(expectedType); - } - - accessInfo.validate(); - fieldAccesses.add(accessInfo); - marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, accessInfo)); - } else { - marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, fInfo)); + boolean isStaticAccessFirst = false; + if (receiverType != null) { + String recvName = receiverType.getSimpleName(); + if (recvName != null && !recvName.isEmpty() && Character.isUpperCase(recvName.charAt(0))) + isStaticAccessFirst = true; } + FieldAccessInfo accessInfoFirst = createFieldAccessInfo(firstField, identStart, identEnd, receiverType, fInfo, !hasMoreSegments, isStaticAccessFirst); + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, accessInfoFirst)); } else { marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR)); } @@ -2435,25 +2417,14 @@ private void markChainedFieldAccesses(List marks) { if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) { FieldInfo segInfo = currentType.getFieldInfo(seg); - if (isLastSegment) { - // Create FieldAccessInfo for validation - boolean isStaticAccess = false; - FieldAccessInfo accessInfo = new FieldAccessInfo( - seg, nStart, nEnd, currentType, segInfo, isStaticAccess - ); - - // Set expected type from assignment context - TypeInfo expectedType = findExpectedTypeAtPosition(nStart); - if (expectedType != null) { - accessInfo.setExpectedType(expectedType); - } - - accessInfo.validate(); - fieldAccesses.add(accessInfo); - marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, accessInfo)); - } else { - marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, segInfo)); + boolean isStaticAccessSeg2 = false; + if (currentType != null) { + String tn = currentType.getSimpleName(); + if (tn != null && !tn.isEmpty() && Character.isUpperCase(tn.charAt(0))) + isStaticAccessSeg2 = true; } + FieldAccessInfo accessInfoSeg = createFieldAccessInfo(seg, nStart, nEnd, currentType, segInfo, isLastSegment, isStaticAccessSeg2); + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, accessInfoSeg)); currentType = (segInfo != null) ? segInfo.getTypeInfo() : null; } else { @@ -2626,6 +2597,25 @@ class OpenBrace { // ==================== UTILITY METHODS ==================== + /** + * Helper to create and validate a FieldAccessInfo. Does NOT add marks or register the info — + * callers should add to `fieldAccesses` and `marks` as appropriate so marking logic remains in-place. + */ + private FieldAccessInfo createFieldAccessInfo(String name, int start, int end, + TypeInfo receiverType, FieldInfo fieldInfo, + boolean isLastSegment, boolean isStaticAccess) { + FieldAccessInfo accessInfo = new FieldAccessInfo(name, start, end, receiverType, fieldInfo, isStaticAccess); + if (isLastSegment) { + TypeInfo expectedType = findExpectedTypeAtPosition(start); + if (expectedType != null) { + accessInfo.setExpectedType(expectedType); + } + } + accessInfo.validate(); + fieldAccesses.add(accessInfo); + return accessInfo; + } + private FieldInfo resolveVariable(String name, int position) { // Find containing method MethodInfo containingMethod = findMethodAtPosition(position); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 8a66f69f0..ac42241b8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -155,6 +155,8 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume token.setMethodCallInfo((MethodCallInfo) mark.metadata); } else if (mark.metadata instanceof ImportData) { token.setImportData((ImportData) mark.metadata); + }else if (mark.metadata instanceof FieldAccessInfo) { + token.setFieldAccessInfo((FieldAccessInfo) mark.metadata); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index f55d906e6..cbbb1645e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -25,7 +25,9 @@ public class Token { private MethodInfo methodInfo; // For method calls/declarations private MethodCallInfo methodCallInfo; // For method calls with argument info private ImportData importData; // For import statements - + private FieldAccessInfo fieldAccessInfo; //For fields like Minecraft.getMinecraft.thePlayer + + // Navigation - set by ScriptLine private Token prev; private Token next; @@ -115,6 +117,7 @@ public static Token undefined(String text, int start, int end) { public FieldInfo getFieldInfo() { return fieldInfo; } public MethodInfo getMethodInfo() { return methodInfo; } public MethodCallInfo getMethodCallInfo() { return methodCallInfo; } + public FieldAccessInfo getFieldAccessInfo() { return fieldAccessInfo; } public ImportData getImportData() { return importData; } public ScriptLine getParentLine() { return parentLine; } @@ -123,6 +126,7 @@ public static Token undefined(String text, int start, int end) { public void setType(TokenType type) { this.type = type; } public void setTypeInfo(TypeInfo info) { this.typeInfo = info; } public void setFieldInfo(FieldInfo info) { this.fieldInfo = info; } + public void setFieldAccessInfo(FieldAccessInfo info) { this.fieldAccessInfo = info; } public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } From 258abeec0ba838cb4d444224001e031ab5f72275 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 19:43:16 +0200 Subject: [PATCH 031/337] Rendered a chained field's correct package (of receiver) --- .../interpreter/hover/TokenHoverInfo.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 5b5d06b41..55fdcf738 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -273,13 +273,18 @@ private void extractMethodCallInfo(Token token) { } if (containingType != null) { - // Show full package.ClassName for context (like IntelliJ) - String pkg = containingType.getPackageName(); - String className = containingType.getSimpleName(); - if (pkg != null && !pkg.isEmpty()) { - packageName = pkg + "." + className; + // Show full qualified class name (like IntelliJ) + String fullName = containingType.getFullName(); + if (fullName != null && !fullName.isEmpty()) { + packageName = fullName; } else { - packageName = className; + String pkg = containingType.getPackageName(); + String className = containingType.getSimpleName(); + if (pkg != null && !pkg.isEmpty()) { + packageName = pkg + "." + className; + } else { + packageName = className; + } } } @@ -313,18 +318,27 @@ private void extractMethodDeclInfo(Token token) { private void extractGlobalFieldInfo(Token token) { FieldInfo fieldInfo = token.getFieldInfo(); - if (fieldInfo == null) return; - + FieldAccessInfo chainedField = token.getFieldAccessInfo(); + if (fieldInfo == null && chainedField != null) + fieldInfo = chainedField.getResolvedField(); + + if (fieldInfo == null) + return; + iconIndicator = "f"; TypeInfo declaredType = fieldInfo.getDeclaredType(); if (declaredType != null) { // Show field's type package.ClassName for context String pkg = declaredType.getPackageName(); - String className = declaredType.getSimpleName(); - if (pkg != null && !pkg.isEmpty()) { + + // For Minecraft.getMinecraft.thePlayer + // Return net.minecraft.client.Minecraft + if (chainedField != null) + pkg = chainedField.getReceiverType().getFullName(); + + if (pkg != null && !pkg.isEmpty()) packageName = pkg; - } // Type - check for actual type color int typeColor = getColorForTypeInfo(declaredType); From e0b4aece908cd3e860d39edb255564d676ae8f44 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 19:43:43 +0200 Subject: [PATCH 032/337] Changed HoverInfo's package color --- .../gui/util/script/interpreter/hover/TokenHoverRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index aa0107472..ab203b920 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -48,7 +48,7 @@ public class TokenHoverRenderer { private static final int BORDER_COLOR = 0xFF3C3F41; /** Package text color */ - private static final int PACKAGE_COLOR = 0xFF808080; + private static int PACKAGE_COLOR = 0xFF6490e2; /** Error text color */ private static final int ERROR_COLOR = 0xFFFF6B68; From 25800e9c3dca86523932669507e2dc4eb0a921eb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 21:28:48 +0200 Subject: [PATCH 033/337] Fixed token position calculation under mouse being inconsistent under single character tokens "y" --- .../npcs/client/gui/util/GuiScriptTextArea.java | 3 ++- .../gui/util/script/interpreter/ScriptLine.java | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) 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 028f46293..9a29c6018 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -14,6 +14,7 @@ import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; import noppes.npcs.client.gui.util.script.interpreter.Token; +import noppes.npcs.client.gui.util.script.interpreter.TokenType; import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; import noppes.npcs.client.gui.util.script.interpreter.hover.TokenHoverRenderer; import noppes.npcs.client.key.impl.ScriptEditorKeys; @@ -997,7 +998,7 @@ private Object[] getTokenAtScreenPosition(int xMouse, int yMouse) { // Get the token at this position int globalMouseX = getSelectionPos(xMouse,yMouse); - Token token = scriptContainer.getInterpreterTokenAt(globalMouseX); + Token token = lineData.getTokenAt(globalMouseX, (t) -> t.getType() != TokenType.DEFAULT); // Ignore default tokens i.e. whitespaces if (token == null) return null; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index ac42241b8..d08751a68 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Predicate; /** * Represents a single line of source code with its tokens. @@ -70,12 +71,20 @@ public Token getLastToken() { } public Token getTokenAt(int globalPosition) { - for (Token t : tokens) { - if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd()) { + for (Token t : tokens) + if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd()) return t; - } - } + + return null; + } + + public Token getTokenAt(int globalPosition, Predicate condition) { + for (Token t : tokens) + if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd() && condition.test(t)) + return t; + return null; + } // ==================== TOKEN MANAGEMENT ==================== From 50165677c7589add4408a1ffcebf059f9c27d787 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 21:38:22 +0200 Subject: [PATCH 034/337] Fixed HoverInfo not being created for method declarations (as MethodInfo was never assigned to mark creation) --- .../util/script/interpreter/ScriptDocument.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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 974e25b29..9e04e3b63 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 @@ -1267,10 +1267,20 @@ private void markMethodDeclarations(List marks) { continue; } + // Find the corresponding MethodInfo created in parseMethodDeclarations + int methodDeclStart = m.start(); + MethodInfo methodInfo = null; + for (MethodInfo method : methods) { + if (method.getDeclarationOffset() == methodDeclStart) { + methodInfo = method; + break; + } + } + // Return type marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); - // Method name - marks.add(new ScriptLine.Mark(m.start(2), m.end(2), TokenType.METHOD_DECL)); + // Method name with MethodInfo metadata + marks.add(new ScriptLine.Mark(m.start(2), m.end(2), TokenType.METHOD_DECL, methodInfo)); } } From fc9c9e187e4957942770d30a80b55677ca263cfd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 22:16:54 +0200 Subject: [PATCH 035/337] Added FieldAccessInfo errors to hover info --- .../client/gui/util/script/interpreter/FieldAccessInfo.java | 5 +++-- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java index 4f169fd7c..20cff67ea 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java @@ -93,8 +93,9 @@ public void validate() { if (expectedType != null && resolvedField != null) { TypeInfo fieldType = resolvedField.getDeclaredType(); if (fieldType != null && !isTypeCompatible(fieldType, expectedType)) { - setError(ErrorType.TYPE_MISMATCH, - "Required type: " + expectedType.getSimpleName() + ", Provided: " + fieldType.getSimpleName()); + //extra space is necessary for alignment + setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+ + "\nRequired: " + expectedType.getSimpleName()); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 55fdcf738..9d24d74fc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -171,6 +171,11 @@ private void extractErrors(Token token) { } } + FieldAccessInfo fieldAccessInfo = token.getFieldAccessInfo(); + if (fieldAccessInfo != null && fieldAccessInfo.hasError()) { + errors.add(fieldAccessInfo.getErrorMessage()); + } + FieldInfo fieldInfo = token.getFieldInfo(); if (fieldInfo != null && !fieldInfo.isResolved()) { errors.add("Cannot resolve symbol '" + token.getText() + "'"); From 8a0c7c5a65642b6b245de8b28f854c994edcd7b6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 22:55:13 +0200 Subject: [PATCH 036/337] Added method call arg errors to all errored args --- .../interpreter/hover/TokenHoverInfo.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 9d24d74fc..5d20feff1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -153,6 +153,21 @@ public static TokenHoverInfo fromToken(Token token) { // ==================== EXTRACTION METHODS ==================== private void extractErrors(Token token) { + // Check if this token is part of a method call argument (positional lookup) + MethodCallInfo containingCall = findMethodCallContainingPosition(token); + if (containingCall != null) { + MethodCallInfo.Argument containingArg = findArgumentContainingPosition(containingCall, token.getGlobalStart()); + if (containingArg != null) { + // Show only this argument's specific error + MethodCallInfo.ArgumentTypeError argError = findArgumentError(containingCall, containingArg); + if (argError != null) { + errors.add(argError.getMessage()); + return; // Only show argument error, not method-level errors + } + } + } + + // Show method-level errors if this is the method name itself MethodCallInfo callInfo = token.getMethodCallInfo(); if (callInfo != null) { if (callInfo.hasArgCountError()) { @@ -171,17 +186,77 @@ private void extractErrors(Token token) { } } + // Show field access errors FieldAccessInfo fieldAccessInfo = token.getFieldAccessInfo(); if (fieldAccessInfo != null && fieldAccessInfo.hasError()) { errors.add(fieldAccessInfo.getErrorMessage()); } + // Show unresolved field errors FieldInfo fieldInfo = token.getFieldInfo(); if (fieldInfo != null && !fieldInfo.isResolved()) { errors.add("Cannot resolve symbol '" + token.getText() + "'"); } } + /** + * Find the method call that contains this token's position within its argument list. + */ + private MethodCallInfo findMethodCallContainingPosition(Token token) { + ScriptLine line = token.getParentLine(); + if (line == null || line.getParent() == null) { + return null; + } + + ScriptDocument doc = line.getParent(); + int tokenStart = token.getGlobalStart(); + + for (MethodCallInfo call : doc.getMethodCalls()) { + // Check if token is within the argument list + if (tokenStart >= call.getOpenParenOffset() && tokenStart <= call.getCloseParenOffset()) { + // Make sure it's not the method name itself + if (tokenStart >= call.getMethodNameStart() && tokenStart <= call.getMethodNameEnd()) { + continue; + } + return call; + } + } + return null; + } + + /** + * Find the argument that contains the given position. + */ + private MethodCallInfo.Argument findArgumentContainingPosition(MethodCallInfo callInfo, int position) { + for (MethodCallInfo.Argument arg : callInfo.getArguments()) { + if (position >= arg.getStartOffset() && position <= arg.getEndOffset()) { + return arg; + } + } + return null; + } + + /** + * Find the type error for a specific argument. + */ + private MethodCallInfo.ArgumentTypeError findArgumentError(MethodCallInfo callInfo, MethodCallInfo.Argument argument) { + if (!callInfo.hasArgTypeError()) { + return null; + } + + int argIndex = callInfo.getArguments().indexOf(argument); + if (argIndex < 0) { + return null; + } + + for (MethodCallInfo.ArgumentTypeError error : callInfo.getArgumentTypeErrors()) { + if (error.getArgIndex() == argIndex) { + return error; + } + } + return null; + } + private int getExpectedArgCount(MethodCallInfo callInfo) { MethodInfo method = callInfo.getResolvedMethod(); if (method != null) { From c591a918570deb0985d537c22899b3d82a61a8a4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 23:10:36 +0200 Subject: [PATCH 037/337] Fixed primitive types not being properly resolved --- .../gui/util/script/interpreter/ScriptDocument.java | 10 ++++++++++ .../gui/util/script/interpreter/TypeResolver.java | 10 ++++++++++ 2 files changed, 20 insertions(+) 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 9e04e3b63..802382b82 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 @@ -804,6 +804,16 @@ private TypeInfo resolveTypeAndTrackUsage(String typeName) { // Strip array brackets baseName = baseName.replace("[]", "").trim(); + // Check primitive types first + if (TypeResolver.isPrimitiveType(baseName)) { + return TypeInfo.forPrimitive(baseName); + } + + // Check for String (common case) + if (baseName.equals("String")) { + return typeResolver.resolveFullName("java.lang.String"); + } + // Check script-defined types FIRST if (scriptTypes.containsKey(baseName)) { return scriptTypes.get(baseName); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java index a962ec9e2..80bbea054 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java @@ -370,6 +370,16 @@ private void parseGenericTypesRecursive(String content, int baseOffset, } } + + /** + * Check if a type name is a primitive type. + */ + public static boolean isPrimitiveType(String typeName) { + return typeName.equals("boolean") || typeName.equals("byte") || typeName.equals("char") || + typeName.equals("short") || typeName.equals("int") || typeName.equals("long") || + typeName.equals("float") || typeName.equals("double") || typeName.equals("void"); + } + /** * Represents a type occurrence within generic content. */ From 662d843147cbb2af20f22b4a9fc4e5fc8c556d27 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 23:11:12 +0200 Subject: [PATCH 038/337] Small refactor --- .../util/script/interpreter/ScriptDocument.java | 16 ++++++++-------- .../gui/util/script/interpreter/TypeInfo.java | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) 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 802382b82..10863298a 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 @@ -806,7 +806,7 @@ private TypeInfo resolveTypeAndTrackUsage(String typeName) { // Check primitive types first if (TypeResolver.isPrimitiveType(baseName)) { - return TypeInfo.forPrimitive(baseName); + return TypeInfo.fromPrimitive(baseName); } // Check for String (common case) @@ -1610,12 +1610,12 @@ private TypeInfo resolveExpressionType(String expr, int position) { // Character literals if (expr.startsWith("'") && expr.endsWith("'")) { - return TypeInfo.forPrimitive("char"); + return TypeInfo.fromPrimitive("char"); } // Boolean literals if (expr.equals("true") || expr.equals("false")) { - return TypeInfo.forPrimitive("boolean"); + return TypeInfo.fromPrimitive("boolean"); } // Null literal @@ -1626,18 +1626,18 @@ private TypeInfo resolveExpressionType(String expr, int position) { // Numeric literals if (expr.matches("-?\\d+[lL]?")) { if (expr.toLowerCase().endsWith("l")) { - return TypeInfo.forPrimitive("long"); + return TypeInfo.fromPrimitive("long"); } - return TypeInfo.forPrimitive("int"); + return TypeInfo.fromPrimitive("int"); } if (expr.matches("-?\\d+\\.\\d*[fF]?")) { if (expr.toLowerCase().endsWith("f")) { - return TypeInfo.forPrimitive("float"); + return TypeInfo.fromPrimitive("float"); } - return TypeInfo.forPrimitive("double"); + return TypeInfo.fromPrimitive("double"); } if (expr.matches("-?\\d+\\.\\d*[dD]")) { - return TypeInfo.forPrimitive("double"); + return TypeInfo.fromPrimitive("double"); } // "this" keyword diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java index a55d8c750..862e5f108 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -98,7 +98,7 @@ public static TypeInfo fromClass(Class clazz) { /** * Create a TypeInfo for a primitive type. */ - public static TypeInfo forPrimitive(String typeName) { + public static TypeInfo fromPrimitive(String typeName) { Class primitiveClass = null; switch (typeName) { case "boolean": primitiveClass = boolean.class; break; From d9d89a2f231cbc9bfe997a2eacb528ca47868f9c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 23:15:14 +0200 Subject: [PATCH 039/337] Primitive type class color --- .../client/gui/util/script/interpreter/hover/TokenHoverInfo.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 5d20feff1..2f24db15c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -601,6 +601,7 @@ private void extractJavadoc(Method method) { * Get the appropriate color for a Class based on its type. */ private int getColorForClass(Class clazz) { + if(clazz.isPrimitive()) return TokenType.KEYWORD.getHexColor(); if (clazz.isInterface()) return TokenType.INTERFACE_DECL.getHexColor(); if (clazz.isEnum()) return TokenType.ENUM_DECL.getHexColor(); return TokenType.IMPORTED_CLASS.getHexColor(); From 026a3dc8d3ec592e57f1df0773132d4b1d2f8d02 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 26 Dec 2025 23:23:38 +0200 Subject: [PATCH 040/337] Fixed modifiers breaking global field declaration type --- .../script/interpreter/ScriptDocument.java | 26 ++++++++++++++++++- .../util/script/interpreter/TypeResolver.java | 10 +++++++ 2 files changed, 35 insertions(+), 1 deletion(-) 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 10863298a..490743a97 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 @@ -764,10 +764,13 @@ private void parseGlobalFields() { if (isExcluded(m.start())) continue; - String typeName = m.group(1); + String typeNameRaw = m.group(1); String fieldName = m.group(2); int position = m.start(2); + // Strip modifiers (public, private, protected, static, final, etc.) from type name + String typeName = stripModifiers(typeNameRaw); + // Check if inside a method - if so, it's a local, not global boolean insideMethod = false; for (MethodInfo method : methods) { @@ -785,6 +788,27 @@ private void parseGlobalFields() { } } + /** + * Strip Java modifiers from a type name string. + * e.g., "public static String" -> "String" + */ + private String stripModifiers(String typeName) { + if (typeName == null) return null; + + String[] parts = typeName.trim().split("\\s+"); + // Filter out known modifiers + StringBuilder result = new StringBuilder(); + for (String part : parts) { + if (!TypeResolver.isModifier(part)) { + if (result.length() > 0) result.append(" "); + result.append(part); + } + } + return result.toString(); + } + + + private TypeInfo resolveType(String typeName) { return resolveTypeAndTrackUsage(typeName); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java index 80bbea054..fb1c2d068 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java @@ -379,6 +379,16 @@ public static boolean isPrimitiveType(String typeName) { typeName.equals("short") || typeName.equals("int") || typeName.equals("long") || typeName.equals("float") || typeName.equals("double") || typeName.equals("void"); } + + /** + * Check if a word is a Java modifier keyword. + */ + public static boolean isModifier(String word) { + return word.equals("public") || word.equals("private") || word.equals("protected") || + word.equals("static") || word.equals("final") || word.equals("abstract") || + word.equals("synchronized") || word.equals("volatile") || word.equals("transient") || + word.equals("native") || word.equals("strictfp"); + } /** * Represents a type occurrence within generic content. From bc3e8cb28e28efcc45cacfe22081dee4f9d64bd3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 00:08:12 +0200 Subject: [PATCH 041/337] Refactored TokenType.NUMBER to LITERAL and added error hover infos for it --- .../gui/util/script/interpreter/ScriptDocument.java | 13 ++----------- .../script/interpreter/ScriptTextContainer.java | 2 +- .../client/gui/util/script/interpreter/Token.java | 6 ++---- .../gui/util/script/interpreter/TokenType.java | 4 ++-- .../script/interpreter/hover/TokenHoverInfo.java | 5 ++++- 5 files changed, 11 insertions(+), 19 deletions(-) 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 490743a97..d0c0730b1 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 @@ -895,8 +895,8 @@ private List buildMarks() { // Methods markMethodDeclarations(marks); - // Numbers - addPatternMarks(marks, NUMBER_PATTERN, TokenType.NUMBER); + // Numbers and othe + addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL); // Method calls (parse before variables so we can attach context) markMethodCalls(marks); @@ -1654,15 +1654,6 @@ private TypeInfo resolveExpressionType(String expr, int position) { } return TypeInfo.fromPrimitive("int"); } - if (expr.matches("-?\\d+\\.\\d*[fF]?")) { - if (expr.toLowerCase().endsWith("f")) { - return TypeInfo.fromPrimitive("float"); - } - return TypeInfo.fromPrimitive("double"); - } - if (expr.matches("-?\\d+\\.\\d*[dD]")) { - return TypeInfo.fromPrimitive("double"); - } // "this" keyword if (expr.equals("this")) { 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 5c98ca2f5..9e3f83b96 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 @@ -136,7 +136,7 @@ private JavaTextContainer.TokenType toLegacyTokenType(noppes.npcs.client.gui.uti if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.TYPE_DECL) return JavaTextContainer.TokenType.TYPE_DECL; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_DECL) return JavaTextContainer.TokenType.METHOD_DECARE; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_CALL) return JavaTextContainer.TokenType.METHOD_CALL; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.NUMBER) return JavaTextContainer.TokenType.NUMBER; + if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.LITERAL) return JavaTextContainer.TokenType.NUMBER; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.GLOBAL_FIELD) return JavaTextContainer.TokenType.GLOBAL_FIELD; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.LOCAL_FIELD) return JavaTextContainer.TokenType.LOCAL_FIELD; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.PARAMETER) return JavaTextContainer.TokenType.PARAMETER; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index cbbb1645e..06aa821a7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -1,7 +1,5 @@ package noppes.npcs.client.gui.util.script.interpreter; -import noppes.npcs.client.ClientProxy; - /** * Represents a single token in the source code with its type and metadata. * Tokens are the atomic units of the syntax highlighting system. @@ -62,8 +60,8 @@ public static Token string(String text, int start, int end) { return new Token(text, start, end, TokenType.STRING); } - public static Token number(String text, int start, int end) { - return new Token(text, start, end, TokenType.NUMBER); + public static Token literal(String text, int start, int end) { + return new Token(text, start, end, TokenType.LITERAL); } public static Token typeReference(String text, int start, int end, TypeInfo info) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java index 24c8c059c..acbace01d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -36,7 +36,7 @@ public enum TokenType { LOCAL_FIELD(0xFFFF55, 25), // local variables (yellow) // Literals - NUMBER(0x777777, 40), // numeric literals + LITERAL(0x777777, 40), // numeric and boolean literals // Default VARIABLE(0xFFFFFF, 30), // generic variables @@ -65,7 +65,7 @@ public int getPriority() { public char toColorCode() { switch (this) { case COMMENT: - case NUMBER: + case LITERAL: case UNUSED_IMPORT: return '7'; // gray case STRING: diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 2f24db15c..9e5e79071 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -125,10 +125,13 @@ public static TokenHoverInfo fromToken(Token token) { info.extractUndefinedInfo(token); break; + case LITERAL: + if (info.hasErrors()) + return info; + return null; case KEYWORD: case MODIFIER: case STRING: - case NUMBER: case COMMENT: // These don't need hover info typically return null; From 2ffe95dc0f0fd85443787f6c8141376f2842d081 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 00:08:49 +0200 Subject: [PATCH 042/337] Fixed parameters being resolved as args --- .../script/interpreter/ScriptDocument.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) 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 d0c0730b1..b278efe11 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 @@ -1574,7 +1574,8 @@ private List parseMethodArguments(int start, int end) { } // Try to resolve the argument type - TypeInfo argType = resolveExpressionType(argText, actualStart); + // First check if this looks like a parameter declaration (Type varName) + TypeInfo argType = resolveArgumentType(argText, actualStart); args.add(new MethodCallInfo.Argument( argText, actualStart, actualEnd, argType, true, null @@ -1607,6 +1608,35 @@ private List parseMethodArguments(int start, int end) { return args; } + /** + * Resolve the type of a method call argument. + * Handles both parameter declarations (Type varName) and expressions (variable.field()). + */ + private TypeInfo resolveArgumentType(String argText, int position) { + argText = argText.trim(); + + // Check if this looks like a parameter declaration: "Type varName" + // Pattern: identifier followed by whitespace and another identifier + if (argText.matches("^[A-Za-z_][a-zA-Z0-9_<>\\[\\],\\s]*\\s+[a-zA-Z_][a-zA-Z0-9_]*$")) { + // Split into tokens + String[] parts = argText.split("\\s+"); + if (parts.length >= 2) { + // Last part is the variable name, everything before is the type + // Join all but last as the type name + StringBuilder typeBuilder = new StringBuilder(); + for (int i = 0; i < parts.length - 1; i++) { + if (i > 0) typeBuilder.append(" "); + typeBuilder.append(parts[i]); + } + String typeName = typeBuilder.toString(); + return resolveType(typeName); + } + } + + // Otherwise, treat it as an expression + return resolveExpressionType(argText, position); + } + /** * Comprehensive expression type resolver that handles: * - Literals (strings, numbers, booleans, null) From 1dbeaf2c84d4ddf6f944ac07a118e6e710c21cfb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 00:09:14 +0200 Subject: [PATCH 043/337] Accounted for arrays in expression parsing --- .../script/interpreter/ScriptDocument.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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 b278efe11..f70319d21 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 @@ -1657,6 +1657,11 @@ private TypeInfo resolveExpressionType(String expr, int position) { return null; } + // Invalid expressions starting with brackets + if (expr.startsWith("[") || expr.startsWith("]")) { + return null; // Invalid syntax + } + // String literals if (expr.startsWith("\"") && expr.endsWith("\"")) { return resolveType("String"); @@ -1809,6 +1814,23 @@ private List parseExpressionChain(String expr) { } } + // Check if followed by array brackets (array access) + // Skip array accesses like [0] or [i] - treat them as part of the current segment + while (i < expr.length() && Character.isWhitespace(expr.charAt(i))) { + i++; + } + if (i < expr.length() && expr.charAt(i) == '[') { + // Skip to the matching closing bracket + int depth = 1; + i++; + while (i < expr.length() && depth > 0) { + char c = expr.charAt(i); + if (c == '[') depth++; + else if (c == ']') depth--; + i++; + } + } + segments.add(new ChainSegment(name, start, i, isMethodCall)); // Skip whitespace From 2e99dffc337a2e4d89d3a07d54e57fdc3643bdbd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 00:16:07 +0200 Subject: [PATCH 044/337] Auto casted 1f, 1.0f to to floats same for doubles --- .../script/interpreter/ScriptDocument.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 f70319d21..637b2cfda 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 @@ -1683,10 +1683,20 @@ private TypeInfo resolveExpressionType(String expr, int position) { } // Numeric literals - if (expr.matches("-?\\d+[lL]?")) { - if (expr.toLowerCase().endsWith("l")) { - return TypeInfo.fromPrimitive("long"); - } + // Float: can be 10f, 10.5f, 10.f, .5f + if (expr.matches("-?\\d*\\.?\\d+[fF]")) { + return TypeInfo.fromPrimitive("float"); + } + // Double: can be 10d, 10.5d, 10.5, .5, 10., but NOT plain integers + if (expr.matches("-?\\d*\\.\\d+[dD]?") || expr.matches("-?\\d+\\.[dD]?") || expr.matches("-?\\d+[dD]")) { + return TypeInfo.fromPrimitive("double"); + } + // Long: 10L or 10l + if (expr.matches("-?\\d+[lL]")) { + return TypeInfo.fromPrimitive("long"); + } + // Int: plain integers without suffix + if (expr.matches("-?\\d+")) { return TypeInfo.fromPrimitive("int"); } From c694785de3f55167ddc51966a33c4b772906f785 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 00:25:19 +0200 Subject: [PATCH 045/337] Accounted for float/double decimal place precision --- .../script/interpreter/ScriptDocument.java | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) 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 637b2cfda..0070ab684 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 @@ -1636,6 +1636,48 @@ private TypeInfo resolveArgumentType(String argText, int position) { // Otherwise, treat it as an expression return resolveExpressionType(argText, position); } + + /** + * Check if a numeric literal has excessive precision for its type. + * Counts significant digits (excluding leading zeros, decimal point, and suffix). + * + * @param numLiteral The numeric literal string (e.g., "1.23456789f", "0.00123456789") + * @param maxDigits Maximum significant digits allowed (7 for float, 15 for double) + * @return true if the literal has more significant digits than allowed + */ + private boolean hasExcessivePrecision(String numLiteral, int maxDigits) { + String cleaned = numLiteral.trim(); + + // Remove leading sign + if (cleaned.startsWith("-") || cleaned.startsWith("+")) { + cleaned = cleaned.substring(1); + } + + // Remove suffix (f, F, d, D, l, L) + if (cleaned.endsWith("f") || cleaned.endsWith("F") || + cleaned.endsWith("d") || cleaned.endsWith("D") || + cleaned.endsWith("l") || cleaned.endsWith("L")) { + cleaned = cleaned.substring(0, cleaned.length() - 1); + } + + // Remove decimal point for counting + cleaned = cleaned.replace(".", ""); + + // Remove leading zeros (they're not significant in the mantissa) + while (cleaned.startsWith("0") && cleaned.length() > 1) { + cleaned = cleaned.substring(1); + } + + // If it's just "0", that's fine + if (cleaned.equals("0") || cleaned.isEmpty()) { + return false; + } + + // Count significant digits + int significantDigits = cleaned.length(); + + return significantDigits > maxDigits; + } /** * Comprehensive expression type resolver that handles: @@ -1682,13 +1724,23 @@ private TypeInfo resolveExpressionType(String expr, int position) { return null; // null is compatible with any reference type } - // Numeric literals + // Numeric literals with precision checking // Float: can be 10f, 10.5f, 10.f, .5f if (expr.matches("-?\\d*\\.?\\d+[fF]")) { + // Check if it has too many decimal places for float (>7 significant digits) + // If so, treat it as double (causing type mismatch) + if (hasExcessivePrecision(expr, 7)) { + return TypeInfo.fromPrimitive("double"); + } return TypeInfo.fromPrimitive("float"); } // Double: can be 10d, 10.5d, 10.5, .5, 10., but NOT plain integers if (expr.matches("-?\\d*\\.\\d+[dD]?") || expr.matches("-?\\d+\\.[dD]?") || expr.matches("-?\\d+[dD]")) { + // Check if it has too many decimal places for double (>15 significant digits) + // Return null to indicate the literal is invalid/unrepresentable + if (hasExcessivePrecision(expr, 15)) { + return null; // Exceeds double precision + } return TypeInfo.fromPrimitive("double"); } // Long: 10L or 10l From a7ed8549a927ccaf1c02152c2e1f44d53be9e3d5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 00:40:58 +0200 Subject: [PATCH 046/337] Added hover info modifiers for script method declarations/fields and chained fields --- .../interpreter/hover/TokenHoverInfo.java | 105 +++++++++++++++++- 1 file changed, 101 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 9e5e79071..c0948f427 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -2,6 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.*; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; @@ -409,8 +410,33 @@ private void extractGlobalFieldInfo(Token token) { return; iconIndicator = "f"; - + + TypeInfo declaredType = fieldInfo.getDeclaredType(); + + // Try to get modifiers from Java reflection if this is a chained field + boolean foundModifiers = false; + if (chainedField != null && chainedField.getReceiverType() != null) { + TypeInfo receiverType = chainedField.getReceiverType(); + if (receiverType.getJavaClass() != null) { + try { + Field javaField = receiverType.getJavaClass().getField(fieldInfo.getName()); + if (javaField != null) + addFieldModifiers(javaField.getModifiers()); + foundModifiers = true; + } catch (Exception e) { + } + } + } + + if (!foundModifiers && fieldInfo.getDeclarationOffset() >= 0) { + // Show modifiers from source if we have a declaration position + String modifiers = extractModifiersAtPosition(fieldInfo.getDeclarationOffset()); + if (modifiers != null && !modifiers.isEmpty()) { + addSegment(modifiers + " ", TokenType.MODIFIER.getHexColor()); + } + } + if (declaredType != null) { // Show field's type package.ClassName for context String pkg = declaredType.getPackageName(); @@ -554,9 +580,17 @@ private void buildMethodDeclaration(Method method, TypeInfo containingType) { } private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo containingType) { - // Modifiers - if (methodInfo.isStatic()) { - addSegment("static ", TokenType.MODIFIER.getHexColor()); + // Try to extract modifiers from source if this is a declaration + if (methodInfo.isDeclaration() && methodInfo.getDeclarationOffset() >= 0) { + String modifiers = extractModifiersAtPosition(methodInfo.getDeclarationOffset()); + if (modifiers != null && !modifiers.isEmpty()) { + addSegment(modifiers + " ", TokenType.MODIFIER.getHexColor()); + } + } else { + // Fallback: show static if we know it + if (methodInfo.isStatic()) { + addSegment("static ", TokenType.MODIFIER.getHexColor()); + } } // Return type @@ -600,6 +634,69 @@ private void extractJavadoc(Method method) { } } + /** + * Extract modifiers (public, private, static, final, etc.) before a declaration position. + * Looks backward from the position to find modifiers. + */ + private String extractModifiersAtPosition(int position) { + if (token.getParentLine().getParent() == null) + return null; + + String text = token.getParentLine().getParent().getText(); + if (position < 0 || position >= text.length()) + return null; + + // Look backward from position to find start of line or previous semicolon/brace + int searchStart = position - 1; + while (searchStart >= 0) { + char c = text.charAt(searchStart); + if (c == ';' || c == '{' || c == '}' || c == '\n') { + searchStart++; + break; + } + searchStart--; + } + if (searchStart < 0) + searchStart = 0; + + // Extract text before the declaration + String beforeDecl = text.substring(searchStart, position).trim(); + + // Match modifier keywords + StringBuilder modifiers = new StringBuilder(); + String[] words = beforeDecl.split("\\s+"); + for (String word : words) { + if (TypeResolver.isModifier(word)) { + if (modifiers.length() > 0) + modifiers.append(" "); + modifiers.append(word); + } + } + + return modifiers.toString(); + } + + /** + * Add field modifiers from Java reflection modifiers. + */ + private void addFieldModifiers(int mods) { + if (Modifier.isPublic(mods)) + addSegment("public ", TokenType.MODIFIER.getHexColor()); + else if (Modifier.isProtected(mods)) + addSegment("protected ", TokenType.MODIFIER.getHexColor()); + else if (Modifier.isPrivate(mods)) + addSegment("private ", TokenType.MODIFIER.getHexColor()); + + if (Modifier.isStatic(mods)) + addSegment("static ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isFinal(mods)) + addSegment("final ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isVolatile(mods)) + addSegment("volatile ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isTransient(mods)) + addSegment("transient ", TokenType.MODIFIER.getHexColor()); + } + /** * Get the appropriate color for a Class based on its type. */ From 4d670f31ded0a2d83c0d07c9accc914a55fffcba Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 13:51:11 +0200 Subject: [PATCH 047/337] Removed declaration from unresolved token hover infos --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index c0948f427..29aec7e06 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -723,7 +723,12 @@ private int getColorForTypeInfo(TypeInfo typeInfo) { public String getPackageName() { return packageName; } public String getIconIndicator() { return iconIndicator; } - public List getDeclaration() { return declaration; } + + public List getDeclaration() { + if (getErrors().stream().anyMatch(err -> err.contains("Cannot resolve"))) + return new ArrayList<>(); + return declaration; + } public List getDocumentation() { return documentation; } public List getErrors() { return errors; } public List getAdditionalInfo() { return additionalInfo; } From 448329386e4119fb6f914f819448a7f5271fe9a2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 14:51:00 +0200 Subject: [PATCH 048/337] Added documentation for method/field declarations and displayed it in hover info --- .../util/script/interpreter/FieldInfo.java | 21 ++- .../util/script/interpreter/MethodInfo.java | 20 ++- .../script/interpreter/ScriptDocument.java | 148 +++++++++++++++++- .../interpreter/hover/TokenHoverInfo.java | 34 +++- 4 files changed, 204 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java index 6fd9ff0e1..2415c3fae 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -17,42 +17,48 @@ public enum Scope { private final TypeInfo declaredType; // The declared type (e.g., String, List) private final int declarationOffset; // Where this field was declared in the source private final boolean resolved; + private final String documentation; // Javadoc/comment documentation for this field // For local/parameter fields, track the containing method private final MethodInfo containingMethod; private FieldInfo(String name, Scope scope, TypeInfo declaredType, - int declarationOffset, boolean resolved, MethodInfo containingMethod) { + int declarationOffset, boolean resolved, MethodInfo containingMethod, String documentation) { this.name = name; this.scope = scope; this.declaredType = declaredType; this.declarationOffset = declarationOffset; this.resolved = resolved; this.containingMethod = containingMethod; + this.documentation = documentation; } // Factory methods public static FieldInfo globalField(String name, TypeInfo type, int declOffset) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null); + } + + public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation) { + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation); } public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method); + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null); } public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method); + return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null); } public static FieldInfo unresolved(String name, Scope scope) { - return new FieldInfo(name, scope, null, -1, false, null); + return new FieldInfo(name, scope, null, -1, false, null, null); } /** * Create a FieldInfo from reflection data for method parameters. */ public static FieldInfo reflectionParam(String name, TypeInfo type) { - return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null); + return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null); } /** @@ -61,7 +67,7 @@ public static FieldInfo reflectionParam(String name, TypeInfo type) { public static FieldInfo fromReflection(java.lang.reflect.Field field, TypeInfo containingType) { String name = field.getName(); TypeInfo type = TypeInfo.fromClass(field.getType()); - return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null); + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null); } // Getters @@ -72,6 +78,7 @@ public static FieldInfo fromReflection(java.lang.reflect.Field field, TypeInfo c public int getDeclarationOffset() { return declarationOffset; } public boolean isResolved() { return resolved; } public MethodInfo getContainingMethod() { return containingMethod; } + public String getDocumentation() { return documentation; } public boolean isGlobal() { return scope == Scope.GLOBAL; } public boolean isLocal() { return scope == Scope.LOCAL; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 93f393c40..4371fd907 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -21,11 +21,12 @@ public final class MethodInfo { private final boolean resolved; private final boolean isDeclaration; // true if this is a declaration, false if it's a call private final boolean isStatic; // true if this is a static method + private final String documentation; // Javadoc/comment documentation for this method private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, List parameters, int declarationOffset, int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration, - boolean isStatic) { + boolean isStatic, String documentation) { this.name = name; this.returnType = returnType; this.containingType = containingType; @@ -36,17 +37,23 @@ private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, this.resolved = resolved; this.isDeclaration = isDeclaration; this.isStatic = isStatic; + this.documentation = documentation; } // Factory methods public static MethodInfo declaration(String name, TypeInfo returnType, List params, int declOffset, int bodyStart, int bodyEnd) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, false); + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, false, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, int declOffset, int bodyStart, int bodyEnd, boolean isStatic) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, isStatic); + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, isStatic, null); + } + + public static MethodInfo declaration(String name, TypeInfo returnType, List params, + int declOffset, int bodyStart, int bodyEnd, boolean isStatic, String documentation) { + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, isStatic, documentation); } public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { @@ -56,7 +63,7 @@ public static MethodInfo call(String name, TypeInfo containingType, int paramCou for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false, false); + return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false, false, null); } public static MethodInfo unresolvedCall(String name, int paramCount) { @@ -64,7 +71,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, null, params, -1, -1, -1, false, false, false); + return new MethodInfo(name, null, null, params, -1, -1, -1, false, false, false, null); } /** @@ -83,7 +90,7 @@ public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInf params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, isStatic); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, isStatic, null); } // Getters @@ -99,6 +106,7 @@ public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInf public boolean isDeclaration() { return isDeclaration; } public boolean isCall() { return !isDeclaration; } public boolean isStatic() { return isStatic; } + public String getDocumentation() { return documentation; } /** * Check if a position is inside this method's body. 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 0070ab684..1374281ba 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 @@ -393,8 +393,11 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { continue; } + // Extract documentation before this field + String documentation = extractDocumentationBefore(absPos); + TypeInfo fieldType = resolveType(typeName); - FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos, documentation); scriptType.addField(fieldInfo); } @@ -420,12 +423,15 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { int methodBodyEnd = findMatchingBrace(methodBodyStart); if (methodBodyEnd < 0) methodBodyEnd = bodyEnd; + // Extract documentation before this method + String documentation = extractDocumentationBefore(absPos); + TypeInfo returnType = resolveType(returnTypeName); List params = parseParametersWithPositions(paramList, bodyStart + 1 + mm.start(3)); MethodInfo methodInfo = MethodInfo.declaration( - methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd); + methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd, false, documentation); scriptType.addMethod(methodInfo); } } @@ -472,6 +478,9 @@ private void parseMethodDeclarations() { if (bodyEnd < 0) bodyEnd = text.length(); + // Extract documentation before this method + String documentation = extractDocumentationBefore(m.start()); + // Parse parameters with their actual positions List params = parseParametersWithPositions(paramList, m.start(3)); @@ -481,7 +490,9 @@ private void parseMethodDeclarations() { params, m.start(), bodyStart, - bodyEnd + bodyEnd, + false, + documentation ); methods.add(methodInfo); } @@ -781,8 +792,11 @@ private void parseGlobalFields() { } if (!insideMethod) { + // Extract documentation before this field + String documentation = extractDocumentationBefore(m.start()); + TypeInfo typeInfo = resolveType(typeName); - FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation); globalFields.put(fieldName, fieldInfo); } } @@ -807,7 +821,131 @@ private String stripModifiers(String typeName) { return result.toString(); } - + /** + * Extract documentation comment immediately preceding a position. + * Supports both single-line (//) and multi-line (/* *\/) comment styles. + * Returns null if no documentation is found. + * + * @param position The position of the declaration (e.g., start of method/field) + * @return The documentation text, or null if none found + */ + private String extractDocumentationBefore(int position) { + if (position <= 0) return null; + + // First, skip backwards to get off the current declaration line + // (declarations might have modifiers like 'public' before the matched position) + int searchStart = position - 1; + + // Skip backwards to the previous newline + while (searchStart >= 0 && text.charAt(searchStart) != '\n') { + searchStart--; + } + + if (searchStart < 0) return null; + + // Now we're at a newline before the declaration line + // Skip backwards through any additional whitespace + searchStart--; + while (searchStart >= 0 && Character.isWhitespace(text.charAt(searchStart))) { + searchStart--; + } + + if (searchStart < 0) return null; + + // Now searchStart should be at the last non-whitespace character before the declaration line + + // Check if we're at the end of a comment + // Case 1: Multi-line comment ending with */ + if (searchStart > 0 && text.charAt(searchStart) == '/' && text.charAt(searchStart - 1) == '*') { + // Find the start of this comment + int commentEnd = searchStart + 1; + int commentStart = text.lastIndexOf("/*", searchStart - 1); + if (commentStart >= 0) { + String comment = text.substring(commentStart, commentEnd); + return cleanDocumentation(comment); + } + } + + // Case 2: Single-line comments (may be multiple consecutive lines) + StringBuilder doc = new StringBuilder(); + int currentPos = searchStart; + + // Walk backward through consecutive comment lines + while (currentPos >= 0) { + // Find start of current line + int lineStart = currentPos; + while (lineStart > 0 && text.charAt(lineStart - 1) != '\n') { + lineStart--; + } + + // Find end of current line + int lineEnd = currentPos; + while (lineEnd < text.length() && text.charAt(lineEnd) != '\n') { + lineEnd++; + } + + // Extract the line and trim + String line = text.substring(lineStart, lineEnd).trim(); + + // Check if this line is a comment + if (line.startsWith("//") || line.startsWith("#")) { + // Prepend to doc (we're going backwards) + String commentText = line.substring(2).trim(); + if (doc.length() > 0) { + doc.insert(0, "\n"); + } + doc.insert(0, commentText); + + // Move to previous line + if (lineStart > 0) { + currentPos = lineStart - 1; + // Skip to previous line (skip the newline) + while (currentPos >= 0 && text.charAt(currentPos) == '\n') { + currentPos--; + } + } else { + break; + } + } else { + // Not a comment line, stop + break; + } + } + + if (doc.length() > 0) { + return doc.toString(); + } + + return null; + } + + /** + * Clean up documentation comment text by removing comment markers and extra formatting. + */ + private String cleanDocumentation(String comment) { + if (comment == null || comment.isEmpty()) return null; + + // Remove /* and */ markers + comment = comment.replaceAll("^/\\*+\\s*", "").replaceAll("\\s*\\*+/$", ""); + + // Split into lines and clean each one + String[] lines = comment.split("\n"); + StringBuilder cleaned = new StringBuilder(); + + for (String line : lines) { + // Remove leading asterisks and whitespace + line = line.trim().replaceAll("^\\*+\\s*", ""); + if (cleaned.length() > 0 && !line.isEmpty()) { + cleaned.append("\n"); + } + if (!line.isEmpty()) { + cleaned.append(line); + } + } + + String result = cleaned.toString().trim(); + return result.isEmpty() ? null : result; + } private TypeInfo resolveType(String typeName) { return resolveTypeAndTrackUsage(typeName); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 29aec7e06..e73217cda 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -411,6 +411,13 @@ private void extractGlobalFieldInfo(Token token) { iconIndicator = "f"; + // Add documentation if available + if (fieldInfo.getDocumentation() != null && !fieldInfo.getDocumentation().isEmpty()) { + String[] docLines = fieldInfo.getDocumentation().split("\n"); + for (String line : docLines) { + documentation.add(line); + } + } TypeInfo declaredType = fieldInfo.getDeclaredType(); @@ -465,6 +472,14 @@ private void extractLocalFieldInfo(Token token) { iconIndicator = "v"; + // Add documentation if available (though local vars rarely have docs) + if (fieldInfo.getDocumentation() != null && !fieldInfo.getDocumentation().isEmpty()) { + String[] docLines = fieldInfo.getDocumentation().split("\n"); + for (String line : docLines) { + documentation.add(line); + } + } + TypeInfo declaredType = fieldInfo.getDeclaredType(); if (declaredType != null) { int typeColor = getColorForTypeInfo(declaredType); @@ -484,6 +499,14 @@ private void extractParameterInfo(Token token) { iconIndicator = "p"; + // Add documentation if available (args/parameters might have docs from method javadoc) + if (fieldInfo.getDocumentation() != null && !fieldInfo.getDocumentation().isEmpty()) { + String[] docLines = fieldInfo.getDocumentation().split("\n"); + for (String line : docLines) { + documentation.add(line); + } + } + TypeInfo declaredType = fieldInfo.getDeclaredType(); if (declaredType != null) { int typeColor = getColorForTypeInfo(declaredType); @@ -621,8 +644,17 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); } addSegment(")", TokenType.DEFAULT.getHexColor()); - } + // Add documentation if available + if (methodInfo.getDocumentation() != null && !methodInfo.getDocumentation().isEmpty()) { + String[] docLines = methodInfo.getDocumentation().split("\n"); + for (String line : docLines) { + documentation.add(line); + } + } + + } + private void extractJavadoc(Method method) { // Java reflection doesn't provide Javadoc at runtime // We could potentially load it from source files or external documentation From 4b06937385521a92fd457aea411b594c56361ae3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 15:02:57 +0200 Subject: [PATCH 049/337] Added separator line for documentation in hover info --- .../interpreter/hover/TokenHoverRenderer.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index ab203b920..7b8105445 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -30,6 +30,12 @@ public class TokenHoverRenderer { /** Line spacing between rows */ private static final int LINE_SPACING = 2; + /** Separator line height */ + private static final int SEPARATOR_HEIGHT = 1; + + /** Spacing above and below separator */ + private static final int SEPARATOR_SPACING = 5; + /** Vertical offset from token */ private static final int VERTICAL_OFFSET = 4; @@ -185,11 +191,12 @@ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, currentY = drawWrappedSegments(textX, currentY, wrapWidth, declaration); currentY += LINE_SPACING; } - // Draw documentation List docs = info.getDocumentation(); if (!docs.isEmpty()) { - currentY += LINE_SPACING; + // Draw separator line before documentation + Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR); + currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING; for (String doc : docs) { List wrappedLines = wrapText(doc, wrapWidth); for (String line : wrappedLines) { @@ -396,7 +403,8 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) // Documentation List docs = info.getDocumentation(); if (!docs.isEmpty()) { - totalHeight += LINE_SPACING; // Extra space before docs + // Add space for separator line and spacing + totalHeight += SEPARATOR_SPACING + SEPARATOR_HEIGHT; for (String doc : docs) { List wrappedLines = wrapText(doc, contentWidth); totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING); From 50d14d757c066aeb41d012de5baa372881720279 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 15:17:29 +0200 Subject: [PATCH 050/337] /** javadoc stub auto-generation --- .../client/gui/util/GuiScriptTextArea.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) 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 9a29c6018..d32e8fdf7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1437,9 +1437,33 @@ private boolean handleInsertionKeys(int i) { return true; } - // RETURN/ENTER: special handling when preceding char is an opening brace '{' + // RETURN/ENTER: special handling for /** javadoc stub and opening brace '{' if (i == Keyboard.KEY_RETURN) { int cursorPos = selection.getCursorPosition(); + + // Check for /** javadoc stub auto-generation + String before = getSelectionBeforeText(); + if (before.endsWith("/**")) { + // Find current line to get indent level + String indent = ""; + for (LineData ld : this.container.lines) { + if (cursorPos >= ld.start && cursorPos <= ld.end) { + indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); + break; + } + } + + // Generate properly indented javadoc block + String javadocStub = "\n" + indent + " * \n" + indent + " */"; + addText(javadocStub); + + // Position cursor after " * " on the middle line + int newCursorPos = before.length() + 1 + indent.length() + 3; // +1 for \n, +3 for " * " + selection.reset(newCursorPos); + scrollToCursor(); + return true; + } + int prevNonWs = cursorPos - 1; while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace( text.charAt(prevNonWs))) { @@ -1457,7 +1481,6 @@ private boolean handleInsertionKeys(int i) { if (indent == null) indent = ""; String childIndent = indent + " "; - String before = getSelectionBeforeText(); String after = getSelectionAfterText(); int firstNewline = after.indexOf('\n'); From 104a816eef9aa2626d0451d51d2118e927d36348 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 16:03:04 +0200 Subject: [PATCH 051/337] Fixed field declaration unresolving if there's a non-empty comment above its line --- .../script/interpreter/ScriptDocument.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) 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 1374281ba..f520fcc11 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 @@ -50,7 +50,7 @@ public class ScriptDocument { private static final Pattern METHOD_CALL_PATTERN = Pattern.compile( "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); private static final Pattern FIELD_DECL_PATTERN = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>,\\s\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + "\\b([A-Za-z_][a-zA-Z0-9_<>,[ \\t]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)[ \\t]*(=|;)"); private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); // Function parameters (for JS-style scripts) @@ -551,17 +551,19 @@ private void parseLocalVariables() { // Pattern for local variable declarations: Type varName = or Type varName; // Allows capital var names like "Minecraft Capital = new Minecraft();" + // Use [ \t] instead of \s to prevent matching across newlines Pattern localDecl = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(=|;|,)"); + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)[ \\t]+([A-Za-z_][a-zA-Z0-9_]*)[ \\t]*(=|;|,)"); Matcher m = localDecl.matcher(bodyText); while (m.find()) { - int absPos = bodyStart + m.start(); - if (isExcluded(absPos)) continue; - String typeName = m.group(1); String varName = m.group(2); String delimiter = m.group(3); + // Skip if the variable name itself is excluded + int absPos = bodyStart + m.start(2); + if (isExcluded(absPos)) continue; + // Skip if it looks like a method call or control flow if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || @@ -772,12 +774,13 @@ private TypeInfo inferChainType(String firstIdent, int identStart, int dotPositi private void parseGlobalFields() { Matcher m = FIELD_DECL_PATTERN.matcher(text); while (m.find()) { - if (isExcluded(m.start())) - continue; - String typeNameRaw = m.group(1); String fieldName = m.group(2); int position = m.start(2); + + // Skip if the field name itself is excluded + if (isExcluded(position)) + continue; // Strip modifiers (public, private, protected, static, final, etc.) from type name String typeName = stripModifiers(typeNameRaw); From c4bbf6108b4d0ea066065c65c99005d6570798e6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 16:08:21 +0200 Subject: [PATCH 052/337] Changed method name color in hover info --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index e73217cda..dfa017670 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -584,7 +584,7 @@ private void buildMethodDeclaration(Method method, TypeInfo containingType) { addSegment(" ", TokenType.DEFAULT.getHexColor()); // Method name - addSegment(method.getName(), TokenType.METHOD_CALL.getHexColor()); + addSegment(method.getName(), TokenType.METHOD_DECL.getHexColor()); // Parameters addSegment("(", TokenType.DEFAULT.getHexColor()); @@ -627,7 +627,7 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain } // Method name - addSegment(methodInfo.getName(), TokenType.METHOD_CALL.getHexColor()); + addSegment(methodInfo.getName(), TokenType.METHOD_DECL.getHexColor()); // Parameters addSegment("(", TokenType.DEFAULT.getHexColor()); From a9cd2ec7ef92366c77e1c8130fce2b850f76d889 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 16:30:12 +0200 Subject: [PATCH 053/337] Added init values to declaration of local/global fields and their hover info --- .../util/script/interpreter/FieldInfo.java | 34 ++++++--- .../script/interpreter/ScriptDocument.java | 70 ++++++++++++++++++- .../interpreter/hover/TokenHoverInfo.java | 31 ++++++++ 3 files changed, 125 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java index 2415c3fae..c123af759 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -18,12 +18,17 @@ public enum Scope { private final int declarationOffset; // Where this field was declared in the source private final boolean resolved; private final String documentation; // Javadoc/comment documentation for this field + + // Initialization value range (for displaying "= value" in hover info) + private final int initStart; // Position of '=' or -1 if no initializer + private final int initEnd; // Position after initializer (before ';') or -1 // For local/parameter fields, track the containing method private final MethodInfo containingMethod; private FieldInfo(String name, Scope scope, TypeInfo declaredType, - int declarationOffset, boolean resolved, MethodInfo containingMethod, String documentation) { + int declarationOffset, boolean resolved, MethodInfo containingMethod, + String documentation, int initStart, int initEnd) { this.name = name; this.scope = scope; this.declaredType = declaredType; @@ -31,34 +36,44 @@ private FieldInfo(String name, Scope scope, TypeInfo declaredType, this.resolved = resolved; this.containingMethod = containingMethod; this.documentation = documentation; + this.initStart = initStart; + this.initEnd = initEnd; } // Factory methods public static FieldInfo globalField(String name, TypeInfo type, int declOffset) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null, -1, -1); } public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation, -1, -1); + } + + public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart, int initEnd) { + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation, initStart, initEnd); } public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null); + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, -1, -1); + } + + public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart, int initEnd) { + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, initStart, initEnd); } public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null); + return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null, -1, -1); } public static FieldInfo unresolved(String name, Scope scope) { - return new FieldInfo(name, scope, null, -1, false, null, null); + return new FieldInfo(name, scope, null, -1, false, null, null, -1, -1); } /** * Create a FieldInfo from reflection data for method parameters. */ public static FieldInfo reflectionParam(String name, TypeInfo type) { - return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null); + return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1); } /** @@ -67,7 +82,7 @@ public static FieldInfo reflectionParam(String name, TypeInfo type) { public static FieldInfo fromReflection(java.lang.reflect.Field field, TypeInfo containingType) { String name = field.getName(); TypeInfo type = TypeInfo.fromClass(field.getType()); - return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null); + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1); } // Getters @@ -79,6 +94,9 @@ public static FieldInfo fromReflection(java.lang.reflect.Field field, TypeInfo c public boolean isResolved() { return resolved; } public MethodInfo getContainingMethod() { return containingMethod; } public String getDocumentation() { return documentation; } + public int getInitStart() { return initStart; } + public int getInitEnd() { return initEnd; } + public boolean hasInitializer() { return initStart >= 0 && initEnd > initStart; } public boolean isGlobal() { return scope == Scope.GLOBAL; } public boolean isLocal() { return scope == Scope.LOCAL; } 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 f520fcc11..8acc8532d 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 @@ -583,8 +583,28 @@ private void parseLocalVariables() { typeInfo = resolveType(typeName); } + // Extract initialization range if there's an '=' delimiter + int initStart = -1; + int initEnd = -1; + if ("=".equals(delimiter)) { + initStart = bodyStart + m.start(3); // Absolute position of '=' + // Find the semicolon or comma that ends this declaration + int searchPos = bodyStart + m.end(3); + int depth = 0; // Track nested parens/brackets/braces + while (searchPos < text.length()) { + char c = text.charAt(searchPos); + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') depth--; + else if ((c == ';' || c == ',') && depth == 0) { + initEnd = searchPos; // Position of ';' or ',' (exclusive) + break; + } + searchPos++; + } + } + int declPos = bodyStart + m.start(2); - FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method); + FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method, initStart, initEnd); locals.put(varName, fieldInfo); } } @@ -776,6 +796,7 @@ private void parseGlobalFields() { while (m.find()) { String typeNameRaw = m.group(1); String fieldName = m.group(2); + String delimiter = m.group(3); int position = m.start(2); // Skip if the field name itself is excluded @@ -798,8 +819,28 @@ private void parseGlobalFields() { // Extract documentation before this field String documentation = extractDocumentationBefore(m.start()); + // Extract initialization range if there's an '=' delimiter + int initStart = -1; + int initEnd = -1; + if ("=".equals(delimiter)) { + initStart = m.start(3); // Position of '=' + // Find the semicolon that ends this declaration + int searchPos = m.end(3); + int depth = 0; // Track nested parens/brackets/braces + while (searchPos < text.length()) { + char c = text.charAt(searchPos); + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') depth--; + else if (c == ';' && depth == 0) { + initEnd = searchPos; // Position of ';' (exclusive) + break; + } + searchPos++; + } + } + TypeInfo typeInfo = resolveType(typeName); - FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation, initStart, initEnd); globalFields.put(fieldName, fieldInfo); } } @@ -3061,6 +3102,31 @@ public int getLineIndexAt(int globalPosition) { } return -1; } + + /** + * Get all tokens that fall within a given range [start, end). + * Returns tokens in order. Tokens that span across the range boundaries + * are included if any part of them is within the range. + */ + public List getTokensInRange(int start, int end) { + List result = new ArrayList<>(); + if (start < 0 || end <= start) return result; + + for (ScriptLine line : lines) { + // Skip lines entirely before the range + if (line.getGlobalEnd() <= start) continue; + // Stop if line is entirely after the range + if (line.getGlobalStart() >= end) break; + + for (Token token : line.getTokens()) { + // Check if token overlaps with range + if (token.getGlobalEnd() > start && token.getGlobalStart() < end) { + result.add(token); + } + } + } + return result; + } /** * Find the expected type for an expression at the given position by looking for assignment context. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index dfa017670..6facd698f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -464,6 +464,9 @@ private void extractGlobalFieldInfo(Token token) { // Field name addSegment(fieldInfo.getName(), TokenType.GLOBAL_FIELD.getHexColor()); + + // Add initialization value if available + addInitializationTokens(token, fieldInfo); } private void extractLocalFieldInfo(Token token) { @@ -489,6 +492,9 @@ private void extractLocalFieldInfo(Token token) { addSegment(fieldInfo.getName(), TokenType.LOCAL_FIELD.getHexColor()); + // Add initialization value if available + addInitializationTokens(token, fieldInfo); + // Show it's a local variable additionalInfo.add("Local variable"); } @@ -750,6 +756,31 @@ private int getColorForTypeInfo(TypeInfo typeInfo) { } return TokenType.IMPORTED_CLASS.getHexColor(); } + + /** + * Add initialization tokens from the field's initializer to the declaration. + * Fetches the tokens in the initialization range and adds them with their proper coloring. + */ + private void addInitializationTokens(Token token, FieldInfo fieldInfo) { + if (!fieldInfo.hasInitializer()) return; + + ScriptLine line = token.getParentLine(); + if (line == null || line.getParent() == null) return; + + ScriptDocument doc = line.getParent(); + // Include the semicolon by extending range by 1 + List initTokens = doc.getTokensInRange(fieldInfo.getInitStart(), fieldInfo.getInitEnd() + 1); + + if (initTokens.isEmpty()) return; + + // Add space before '=' for readability + addSegment(" ", TokenType.DEFAULT.getHexColor()); + + // Add each token with its proper color + for (Token initToken : initTokens) { + addSegment(initToken.getText(), initToken.getType().getHexColor()); + } + } // ==================== GETTERS ==================== From 9402ff5ec5eb193c47d000f9b54e5d9c55596014 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 17:58:42 +0200 Subject: [PATCH 054/337] Captured all assignments of global/local fields --- .../script/interpreter/AssignmentInfo.java | 309 +++++++++++++++++ .../util/script/interpreter/FieldInfo.java | 219 +++++++++++- .../script/interpreter/ScriptDocument.java | 323 ++++++++++++++++++ .../util/script/interpreter/ScriptLine.java | 63 +++- .../interpreter/hover/TokenHoverInfo.java | 39 +++ 5 files changed, 938 insertions(+), 15 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java new file mode 100644 index 000000000..e748834c2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java @@ -0,0 +1,309 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +/** + * Metadata for assignment validation. + * Tracks assignment statements like "varName = expr" or "obj.field = expr" + * and validates type compatibility, access modifiers, and final status. + * + * Each AssignmentInfo represents a single assignment to a field. + * The positions are: + * - lhsStart/lhsEnd: The target variable/field position (left-hand side) + * - rhsStart/rhsEnd: From first token of RHS to the semicolon (inclusive) + */ +public class AssignmentInfo { + + /** + * Error type for assignment validation. + */ + public enum ErrorType { + NONE, + TYPE_MISMATCH, // Assigned value type doesn't match target type + FINAL_REASSIGNMENT, // Attempting to reassign a final field + PRIVATE_ACCESS, // Attempting to access private field + PROTECTED_ACCESS, // Attempting to access protected field from invalid context + UNRESOLVED_TARGET, // Target variable/field doesn't exist + STATIC_CONTEXT_ERROR // Accessing instance field from static context + } + + // Target/LHS info + private final String targetName; + private final int lhsStart; // Position of first char of target variable/field + private final int lhsEnd; // Position after last char of target + private final TypeInfo targetType; // Declared type of target + + // Source/RHS info (from first token of RHS expression to semicolon) + private final int rhsStart; // Position of first non-whitespace char after '=' + private final int rhsEnd; // Position of ';' (inclusive in the range for underlining) + private final TypeInfo sourceType; // Resolved type of RHS expression + private final String sourceExpr; // The RHS expression text (for display) + + // For chained field access (e.g., obj.field = value) + private final TypeInfo receiverType; // Type of receiver (null for simple variables) + private final Field reflectionField; // Java reflection field (for modifier checks) + + // Flag for script-defined final variables + private final boolean isFinal; + + // Validation + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + private String requiredType; + private String providedType; + + public AssignmentInfo(String targetName, int lhsStart, int lhsEnd, + TypeInfo targetType, int rhsStart, int rhsEnd, + TypeInfo sourceType, String sourceExpr, + TypeInfo receiverType, java.lang.reflect.Field reflectionField, + boolean isFinal) { + this.targetName = targetName; + this.lhsStart = lhsStart; + this.lhsEnd = lhsEnd; + this.targetType = targetType; + this.rhsStart = rhsStart; + this.rhsEnd = rhsEnd; + this.sourceType = sourceType; + this.sourceExpr = sourceExpr; + this.receiverType = receiverType; + this.reflectionField = reflectionField; + this.isFinal = isFinal; + } + + // ==================== VALIDATION ==================== + + /** + * Validate this assignment. + * Checks type compatibility, final status, and access modifiers. + */ + public void validate() { + // Check for final field reassignment from reflection + if (reflectionField != null && Modifier.isFinal(reflectionField.getModifiers())) { + setError(ErrorType.FINAL_REASSIGNMENT, + "Cannot assign a value to final variable '" + targetName + "'"); + return; + } + + // Check for final field reassignment from FieldInfo (script-defined finals) + if (isFinal) { + setError(ErrorType.FINAL_REASSIGNMENT, + "Cannot assign a value to final variable '" + targetName + "'"); + return; + } + + // Check access modifiers for chained field access + if (reflectionField != null && receiverType != null) { + int mods = reflectionField.getModifiers(); + if (Modifier.isPrivate(mods)) { + setError(ErrorType.PRIVATE_ACCESS, + "'" + targetName + "' has private access in '" + receiverType.getFullName() + "'"); + return; + } + } + + // Check type compatibility + if (targetType != null && sourceType != null) { + if (!isTypeCompatible(sourceType, targetType)) { + this.requiredType = targetType.getSimpleName(); + this.providedType = sourceType.getSimpleName(); + setError(ErrorType.TYPE_MISMATCH, buildTypeMismatchMessage()); + } + } + } + + /** + * Build a formatted type mismatch error message (IntelliJ style). + */ + private String buildTypeMismatchMessage() { + return "Required type: " + requiredType + "\nProvided: " + providedType; + } + + /** + * Check if sourceType can be assigned to targetType. + */ + private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { + if (sourceType == null || targetType == null) { + return true; // Can't validate, assume compatible + } + + // Same type name + if (sourceType.getFullName().equals(targetType.getFullName())) { + return true; + } + + // Same simple name (for primitives and common types) + if (sourceType.getSimpleName().equals(targetType.getSimpleName())) { + return true; + } + + // Check if sourceType is a subtype of targetType via reflection + if (sourceType.isResolved() && targetType.isResolved()) { + Class sourceClass = sourceType.getJavaClass(); + Class targetClass = targetType.getJavaClass(); + + if (sourceClass != null && targetClass != null) { + // Direct assignability + if (targetClass.isAssignableFrom(sourceClass)) { + return true; + } + + // Primitive widening conversions + if (isPrimitiveWidening(sourceClass, targetClass)) { + return true; + } + + // Boxing/unboxing + if (isBoxingCompatible(sourceClass, targetClass)) { + return true; + } + } + } + + return false; + } + + /** + * Check for primitive widening conversions. + * byte -> short -> int -> long -> float -> double + * char -> int -> long -> float -> double + */ + private boolean isPrimitiveWidening(Class from, Class to) { + if (!from.isPrimitive() || !to.isPrimitive()) { + return false; + } + + if (from == byte.class) { + return to == short.class || to == int.class || to == long.class || + to == float.class || to == double.class; + } + if (from == short.class || from == char.class) { + return to == int.class || to == long.class || to == float.class || to == double.class; + } + if (from == int.class) { + return to == long.class || to == float.class || to == double.class; + } + if (from == long.class) { + return to == float.class || to == double.class; + } + if (from == float.class) { + return to == double.class; + } + return false; + } + + /** + * Check for boxing/unboxing compatibility. + */ + private boolean isBoxingCompatible(Class from, Class to) { + if (from.isPrimitive()) { + Class wrapper = getWrapperClass(from); + if (wrapper != null && to.isAssignableFrom(wrapper)) { + return true; + } + } + if (to.isPrimitive()) { + Class wrapper = getWrapperClass(to); + if (wrapper != null && wrapper.isAssignableFrom(from)) { + return true; + } + } + return false; + } + + private Class getWrapperClass(Class primitive) { + if (primitive == boolean.class) return Boolean.class; + if (primitive == byte.class) return Byte.class; + if (primitive == char.class) return Character.class; + if (primitive == short.class) return Short.class; + if (primitive == int.class) return Integer.class; + if (primitive == long.class) return Long.class; + if (primitive == float.class) return Float.class; + if (primitive == double.class) return Double.class; + return null; + } + + private void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + // ==================== POSITION CHECKS ==================== + + /** + * Check if the given global position falls within the LHS (target) of this assignment. + */ + public boolean containsLhsPosition(int position) { + return position >= lhsStart && position < lhsEnd; + } + + /** + * Check if the given global position falls within the RHS (source) of this assignment. + */ + public boolean containsRhsPosition(int position) { + return position >= rhsStart && position <= rhsEnd; + } + + /** + * Check if the given global position falls anywhere in this assignment (LHS or RHS). + */ + public boolean containsPosition(int position) { + return position >= lhsStart && position <= rhsEnd; + } + + // ==================== ERROR TYPE CHECKS ==================== + + /** + * Check if this is an LHS error (final reassignment, access errors). + * These errors should underline the LHS. + */ + public boolean isLhsError() { + return errorType == ErrorType.FINAL_REASSIGNMENT || + errorType == ErrorType.PRIVATE_ACCESS || + errorType == ErrorType.PROTECTED_ACCESS || + errorType == ErrorType.STATIC_CONTEXT_ERROR; + } + + /** + * Check if this is an RHS error (type mismatch). + * These errors should underline the RHS. + */ + public boolean isRhsError() { + return errorType == ErrorType.TYPE_MISMATCH; + } + + // ==================== GETTERS ==================== + + public String getTargetName() { return targetName; } + public int getLhsStart() { return lhsStart; } + public int getLhsEnd() { return lhsEnd; } + public TypeInfo getTargetType() { return targetType; } + public int getRhsStart() { return rhsStart; } + public int getRhsEnd() { return rhsEnd; } + public TypeInfo getSourceType() { return sourceType; } + public String getSourceExpr() { return sourceExpr; } + public TypeInfo getReceiverType() { return receiverType; } + public java.lang.reflect.Field getReflectionField() { return reflectionField; } + + public ErrorType getErrorType() { return errorType; } + public String getErrorMessage() { return errorMessage; } + public String getRequiredType() { return requiredType; } + public String getProvidedType() { return providedType; } + + public boolean hasError() { return errorType != ErrorType.NONE; } + public boolean hasTypeMismatch() { return errorType == ErrorType.TYPE_MISMATCH; } + public boolean hasFinalReassignment() { return errorType == ErrorType.FINAL_REASSIGNMENT; } + public boolean hasAccessError() { return errorType == ErrorType.PRIVATE_ACCESS || errorType == ErrorType.PROTECTED_ACCESS; } + + @Override + public String toString() { + return "AssignmentInfo{" + + "target='" + targetName + "', " + + "lhs=[" + lhsStart + "-" + lhsEnd + "], " + + "rhs=[" + rhsStart + "-" + rhsEnd + "], " + + "targetType=" + targetType + ", " + + "sourceType=" + sourceType + ", " + + "error=" + errorType + + '}'; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java index c123af759..47eb3191a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -1,8 +1,17 @@ package noppes.npcs.client.gui.util.script.interpreter; +import scala.annotation.meta.field; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Metadata for a field (variable) declaration or reference. * Tracks the field name, type, scope, and declaration location. + * Also tracks all assignments made to this field throughout the script. */ public final class FieldInfo { @@ -26,9 +35,22 @@ public enum Scope { // For local/parameter fields, track the containing method private final MethodInfo containingMethod; + // Modifiers for script-defined fields + private final int modifiers; // Java Modifier flags (e.g., Modifier.FINAL) + + // Reflection field for external types + private final Field reflectionField; + + // Assignments to this field (populated after parsing) + private final List assignments = new ArrayList<>(); + + // Declaration assignment (for initial value validation) + private AssignmentInfo declarationAssignment; + private FieldInfo(String name, Scope scope, TypeInfo declaredType, - int declarationOffset, boolean resolved, MethodInfo containingMethod, - String documentation, int initStart, int initEnd) { + int declarationOffset, boolean resolved, MethodInfo containingMethod, + String documentation, int initStart, int initEnd, int modifiers, + Field reflectionField) { this.name = name; this.scope = scope; this.declaredType = declaredType; @@ -38,54 +60,223 @@ private FieldInfo(String name, Scope scope, TypeInfo declaredType, this.documentation = documentation; this.initStart = initStart; this.initEnd = initEnd; + this.modifiers = modifiers; + this.reflectionField = reflectionField; } // Factory methods public static FieldInfo globalField(String name, TypeInfo type, int declOffset) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null, -1, -1); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null, -1, + -1, 0, null); } public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation, -1, -1); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, + documentation, -1, -1, 0, null); } public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart, int initEnd) { - return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, documentation, initStart, initEnd); + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, + documentation, initStart, initEnd, 0, null); + } + + public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart, + int initEnd, int modifiers) { + return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, + documentation, initStart, initEnd, modifiers, null); } public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, -1, -1); + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, -1, + -1, 0, null); } public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart, int initEnd) { - return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, initStart, initEnd); + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, + initStart, initEnd, 0, null); + } + + public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart, + int initEnd, int modifiers) { + return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, + initStart, initEnd, modifiers, null); } public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) { - return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null, -1, -1); + return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null, + -1, -1, 0, null); } public static FieldInfo unresolved(String name, Scope scope) { - return new FieldInfo(name, scope, null, -1, false, null, null, -1, -1); + return new FieldInfo(name, scope, null, -1, false, null, null, -1, -1, 0, null); } /** * Create a FieldInfo from reflection data for method parameters. */ public static FieldInfo reflectionParam(String name, TypeInfo type) { - return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1); + return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1, 0, null); } /** * Create a FieldInfo from reflection data for a class field. */ - public static FieldInfo fromReflection(java.lang.reflect.Field field, TypeInfo containingType) { + public static FieldInfo fromReflection(Field field, TypeInfo containingType) { String name = field.getName(); TypeInfo type = TypeInfo.fromClass(field.getType()); - return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1); + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1, field.getModifiers(), field); + } + + // ==================== ASSIGNMENT MANAGEMENT ==================== + + /** + * Add an assignment to this field. + */ + public void addAssignment(AssignmentInfo assignment) { + assignments.add(assignment); + } + + /** + * Get all assignments to this field (unmodifiable). + */ + public List getAssignments() { + return Collections.unmodifiableList(assignments); + } + + /** + * Set the declaration assignment (initial value assignment). + */ + public void setDeclarationAssignment(AssignmentInfo assignment) { + this.declarationAssignment = assignment; + } + + /** + * Get the declaration assignment (initial value assignment). + * Returns null if there was no initializer or it hasn't been validated yet. + */ + public AssignmentInfo getDeclarationAssignment() { + return declarationAssignment; + } + + /** + * Find an assignment that contains the given position. + * Returns null if no assignment contains this position. + */ + public AssignmentInfo findAssignmentAtPosition(int position) { + // Check declaration assignment first + if (declarationAssignment != null && declarationAssignment.containsPosition(position)) { + return declarationAssignment; + } + + // Check other assignments + for (AssignmentInfo assign : assignments) { + if (assign.containsPosition(position)) { + return assign; + } + } + return null; + } + + /** + * Get all errored assignments for this field. + */ + public List getErroredAssignments() { + List errored = new ArrayList<>(); + + // Include declaration assignment if it has an error + if (declarationAssignment != null && declarationAssignment.hasError()) { + errored.add(declarationAssignment); + } + + // Include all other assignments with errors + for (AssignmentInfo assign : assignments) { + if (assign.hasError()) { + errored.add(assign); + } + } + return errored; + } + + /** + * Clear all assignments (for re-parsing). + */ + public void clearAssignments() { + assignments.clear(); + declarationAssignment = null; + } + + // ==================== MODIFIER CHECKS ==================== + + /** + * Check if this field is declared as final. + * Works for both script-defined fields (via modifiers) and reflection fields. + */ + public boolean isFinal() { + if (reflectionField != null) + return Modifier.isFinal(reflectionField.getModifiers()); + + return Modifier.isFinal(modifiers); + } + + /** + * Check if this field is declared as static. + */ + public boolean isStatic() { + if (reflectionField != null) + return Modifier.isStatic(reflectionField.getModifiers()); + + return Modifier.isStatic(modifiers); + } + + /** + * Check if this field is declared as private. + */ + public boolean isPrivate() { + if (reflectionField != null) + return Modifier.isPrivate(reflectionField.getModifiers()); + + return Modifier.isPrivate(modifiers); } - // Getters + /** + * Check if this field is declared as protected. + */ + public boolean isProtected() { + if (reflectionField != null) + return Modifier.isProtected(reflectionField.getModifiers()); + + return Modifier.isProtected(modifiers); + } + + /** + * Check if this field is declared as public. + */ + public boolean isPublic() { + if (reflectionField != null) + return Modifier.isPublic(reflectionField.getModifiers()); + + return Modifier.isPublic(modifiers); + } + + /** + * Get the raw modifiers value. + */ + public int getModifiers() { + if (reflectionField != null) + return reflectionField.getModifiers(); + + return modifiers; + } + + /** + * Get the reflection field, if available. + */ + public java.lang.reflect.Field getReflectionField() { + return reflectionField; + } + + // ==================== BASIC GETTERS ==================== + public String getName() { return name; } public Scope getScope() { return scope; } public TypeInfo getDeclaredType() { return declaredType; } @@ -135,7 +326,7 @@ public TokenType getTokenType() { @Override public String toString() { - return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + "}"; + return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + ", final=" + isFinal() + "}"; } @Override 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 8acc8532d..fdd9b2bf5 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 @@ -317,6 +317,9 @@ private void parseStructure() { // Parse global fields (outside methods) parseGlobalFields(); + + // Parse and validate assignments (reassignments) - stores in FieldInfo + parseAssignments(); } /** @@ -2766,6 +2769,304 @@ private void markChainedFieldAccesses(List marks) { } } + + /** + * Parse assignment statements (reassignments, not declarations) and validate type compatibility. + * This detects patterns like: varName = expr; or obj.field = expr; + * Assignments are stored in the corresponding FieldInfo, not as marks. + */ + private void parseAssignments() { + // First clear any existing assignments in all FieldInfo objects + for (FieldInfo field : globalFields.values()) { + field.clearAssignments(); + } + for (Map locals : methodLocals.values()) { + for (FieldInfo field : locals.values()) { + field.clearAssignments(); + } + } + + // Pattern to find assignments: identifier = expression; + // But NOT declarations (Type varName = expr) or compound assignments (+=, -=, etc.) + // We need to find '=' that is: + // 1. Not preceded by another operator (!, <, >, =) + // 2. Not followed by '=' + // 3. The LHS must be a valid l-value (variable or field access) + + int pos = 0; + while (pos < text.length()) { + // Find the next '=' character + int equalsPos = text.indexOf('=', pos); + if (equalsPos < 0) break; + + // Skip if in excluded region (string/comment) + if (isExcluded(equalsPos)) { + pos = equalsPos + 1; + continue; + } + + // Check it's not part of ==, !=, <=, >=, +=, -=, *=, /=, %=, &=, |=, ^= + if (equalsPos > 0 && "!<>=+-*/%&|^".indexOf(text.charAt(equalsPos - 1)) >= 0) { + pos = equalsPos + 1; + continue; + } + if (equalsPos < text.length() - 1 && text.charAt(equalsPos + 1) == '=') { + pos = equalsPos + 2; + continue; + } + + // Find the start of this statement (previous ; or { or } or start of text) + int stmtStart = equalsPos - 1; + while (stmtStart >= 0) { + char c = text.charAt(stmtStart); + if (c == ';' || c == '{' || c == '}') { + stmtStart++; + break; + } + stmtStart--; + } + if (stmtStart < 0) stmtStart = 0; + + // Find the end of this statement (next ;) + int stmtEnd = text.indexOf(';', equalsPos); + if (stmtEnd < 0) stmtEnd = text.length(); + + // Extract LHS (before =) and RHS (after =) + String lhsRaw = text.substring(stmtStart, equalsPos).trim(); + String rhsRaw = text.substring(equalsPos + 1, stmtEnd).trim(); + + // Skip empty assignments + if (lhsRaw.isEmpty() || rhsRaw.isEmpty()) { + pos = equalsPos + 1; + continue; + } + + // Check if this is a declaration (has type before variable name) + // Declarations have: Type varName = expr + // Reassignments have: varName = expr or obj.field = expr + boolean isDeclaration = isVariableDeclaration(lhsRaw); + + if (isDeclaration) { + // This is a declaration with initializer - validate the initial value + createAndAttachDeclarationAssignment(lhsRaw, rhsRaw, stmtStart, equalsPos, stmtEnd); + } else { + // This is a reassignment - create AssignmentInfo and attach to FieldInfo + createAndAttachAssignment(lhsRaw, rhsRaw, stmtStart, equalsPos, stmtEnd); + } + + pos = stmtEnd + 1; + } + } + + /** + * Check if the LHS represents a variable declaration (has a type before the variable name). + */ + private boolean isVariableDeclaration(String lhs) { + // Split by whitespace + String[] parts = lhs.trim().split("\\s+"); + + // Single word = reassignment (e.g., "x") + if (parts.length == 1) { + // Could also be a.b = expr (field access) + return false; + } + + // Check if it matches pattern: [modifiers] Type varName + // Last part is the variable name, second-to-last should be a type + String potentialType = parts[parts.length - 2]; + String potentialVar = parts[parts.length - 1]; + + // Type patterns: primitives, or capitalized class names, or generic types + if (isPrimitiveType(potentialType) || + (Character.isUpperCase(potentialType.charAt(0)) && potentialType.matches("[A-Za-z_][A-Za-z0-9_<>\\[\\],\\s]*")) || + potentialType.equals("var") || potentialType.equals("let") || potentialType.equals("const")) { + + // Make sure the variable name is a valid identifier (not a field access chain) + if (potentialVar.matches("[a-zA-Z_][a-zA-Z0-9_]*") && !potentialVar.contains(".")) { + return true; + } + } + + return false; + } + + private boolean isPrimitiveType(String type) { + return type.equals("int") || type.equals("long") || type.equals("short") || type.equals("byte") || + type.equals("float") || type.equals("double") || type.equals("boolean") || type.equals("char") || + type.equals("void"); + } + + /** + * Create an AssignmentInfo for a reassignment statement and attach it to the appropriate FieldInfo. + */ + private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, int equalsPos, int stmtEnd) { + lhs = lhs.trim(); + rhs = rhs.trim(); + + // Parse the target (LHS) + // Could be: varName or obj.field or this.field or array[index] + String targetName = lhs; + FieldInfo targetField = null; + TypeInfo targetType = null; + TypeInfo receiverType = null; + java.lang.reflect.Field reflectionField = null; + + // Find the position where the LHS starts in the actual text (skip leading whitespace) + int lhsStart = stmtStart; + while (lhsStart < equalsPos && Character.isWhitespace(text.charAt(lhsStart))) { + lhsStart++; + } + + // Calculate the actual LHS end (before '=' minus trailing whitespace) + int lhsEnd = equalsPos; + while (lhsEnd > lhsStart && Character.isWhitespace(text.charAt(lhsEnd - 1))) { + lhsEnd--; + } + + // Calculate RHS start (first non-whitespace after '=') + int rhsStart = equalsPos + 1; + while (rhsStart < stmtEnd && Character.isWhitespace(text.charAt(rhsStart))) { + rhsStart++; + } + + // RHS end is at the semicolon position (inclusive for error underline) + int rhsEnd = stmtEnd; + + // Handle chained field access (obj.field) + if (lhs.contains(".")) { + // Split into segments + String[] segments = lhs.split("\\."); + targetName = segments[segments.length - 1].trim(); + + // Resolve the chain to get the target type + String receiverExpr = lhs.substring(0, lhs.lastIndexOf('.')).trim(); + receiverType = resolveExpressionType(receiverExpr, lhsStart); + + if (receiverType != null && receiverType.isResolved()) { + // Get the field from the receiver type + if (receiverType.hasField(targetName)) { + targetField = receiverType.getFieldInfo(targetName); + targetType = targetField.getTypeInfo(); + reflectionField = targetField.getReflectionField(); + } + } + } else { + // Simple variable + targetField = resolveVariable(targetName, lhsStart); + if (targetField != null) { + targetType = targetField.getDeclaredType(); + reflectionField = targetField.getReflectionField(); + } + } + + // If we couldn't resolve the target field, we can't attach the assignment + if (targetField == null) { + return; + } + + // Resolve the source type (RHS) + TypeInfo sourceType = resolveExpressionType(rhs, equalsPos + 1); + + // Create the assignment info using the new constructor + AssignmentInfo info = new AssignmentInfo( + targetName, + lhsStart, + lhsEnd, + targetType, + rhsStart, + rhsEnd, + sourceType, + rhs, + receiverType, + reflectionField, + targetField.isFinal() + ); + + // Validate the assignment + info.validate(); + + // Attach to the FieldInfo + targetField.addAssignment(info); + } + + /** + * Create an AssignmentInfo for a declaration with initializer and attach it to the appropriate FieldInfo. + * Example: String str = 20; or final int x = "test"; + */ + private void createAndAttachDeclarationAssignment(String lhs, String rhs, int stmtStart, int equalsPos, int stmtEnd) { + lhs = lhs.trim(); + rhs = rhs.trim(); + + // Parse the declaration: [modifiers] Type varName + String[] parts = lhs.split("\\s+"); + if (parts.length < 2) { + return; // Invalid declaration format + } + + // Last part is the variable name + String varName = parts[parts.length - 1].trim(); + + // Find the actual position of the variable name in the text + int varNameStart = stmtStart; + int searchStart = stmtStart; + while (searchStart < equalsPos) { + int found = text.indexOf(varName, searchStart); + if (found >= 0 && found < equalsPos) { + // Verify it's the actual variable name (not part of type name) + // Check that it's either at start or preceded by whitespace + if (found == stmtStart || Character.isWhitespace(text.charAt(found - 1))) { + // Check that it's followed by whitespace or '=' + int afterVar = found + varName.length(); + if (afterVar >= text.length() || Character.isWhitespace(text.charAt(afterVar)) || text.charAt(afterVar) == '=') { + varNameStart = found; + break; + } + } + searchStart = found + 1; + } else { + break; + } + } + + int varNameEnd = varNameStart + varName.length(); + + // Calculate RHS positions + int rhsStart = equalsPos + 1; + while (rhsStart < stmtEnd && Character.isWhitespace(text.charAt(rhsStart))) { + rhsStart++; + } + int rhsEnd = stmtEnd; + + // Resolve the target field (should already exist from parseGlobalFields or parseLocalVariables) + FieldInfo targetField = resolveVariable(varName, varNameStart); + if (targetField == null) { + return; // Field doesn't exist, can't attach assignment + } + + TypeInfo targetType = targetField.getDeclaredType(); + TypeInfo sourceType = resolveExpressionType(rhs, rhsStart); + + // Create the assignment info + AssignmentInfo info = new AssignmentInfo( + varName, + varNameStart, + varNameEnd, + targetType, + rhsStart, + rhsEnd, + sourceType, + rhs, + null, // No receiver for simple declarations + targetField.getReflectionField(), + targetField.isFinal() + ); + + // Validate the assignment + info.validate(); + + // Attach as the declaration assignment + targetField.setDeclarationAssignment(info); + } private void markImportedClassUsages(List marks) { // Find uppercase identifiers followed by dot (static method calls, field access) @@ -3071,6 +3372,28 @@ public List getFieldAccesses() { public Map getGlobalFields() { return Collections.unmodifiableMap(globalFields); } + + /** + * Get all errored assignments across all fields (global and local). + * Used by ScriptLine to draw error underlines. + */ + public List getAllErroredAssignments() { + List errored = new ArrayList<>(); + + // Check global fields + for (FieldInfo field : globalFields.values()) { + errored.addAll(field.getErroredAssignments()); + } + + // Check method locals + for (Map locals : methodLocals.values()) { + for (FieldInfo field : locals.values()) { + errored.addAll(field.getErroredAssignments()); + } + } + + return errored; + } public TypeResolver getTypeResolver() { return typeResolver; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index d08751a68..09a6159fe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -164,7 +164,7 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume token.setMethodCallInfo((MethodCallInfo) mark.metadata); } else if (mark.metadata instanceof ImportData) { token.setImportData((ImportData) mark.metadata); - }else if (mark.metadata instanceof FieldAccessInfo) { + } else if (mark.metadata instanceof FieldAccessInfo) { token.setFieldAccessInfo((FieldAccessInfo) mark.metadata); } } @@ -381,6 +381,67 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { } } } + + // Check all errored assignments in the document (stored in FieldInfo objects) + for (AssignmentInfo assign : doc.getAllErroredAssignments()) { + // Determine what to underline based on error type + // LHS errors (final reassignment, access errors) underline the LHS + // RHS errors (type mismatch) underline the RHS + + int underlineStart, underlineEnd; + String underlineText; + + if (assign.isLhsError()) { + // Underline the LHS (target variable) + underlineStart = assign.getLhsStart(); + underlineEnd = assign.getLhsEnd(); + underlineText = assign.getTargetName(); + } else if (assign.isRhsError()) { + // Underline the RHS (source expression) + underlineStart = assign.getRhsStart(); + underlineEnd = assign.getRhsEnd(); + underlineText = assign.getSourceExpr(); + } else { + // Skip assignments with no displayable error + continue; + } + + // Skip if this doesn't intersect this line + if (underlineEnd < lineStart || underlineStart > lineEnd) + continue; + + // Clip to line boundaries + int clipStart = Math.max(underlineStart, lineStart); + int clipEnd = Math.min(underlineEnd, lineEnd); + + if (clipStart >= clipEnd) + continue; + + // Convert to line-local coordinates + int lineLocalStart = clipStart - lineStart; + int lineLocalEnd = clipEnd - lineStart; + + // Bounds check + if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) + continue; + + // Compute pixel position + String beforeUnderline = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeUnderline); + + int underlineWidth; + if (lineLocalEnd > lineText.length()) { + // Extends past line end + underlineWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); + } else { + String underlineOnLine = lineText.substring(lineLocalStart, lineLocalEnd); + underlineWidth = ClientProxy.Font.width(underlineOnLine); + } + + if (underlineWidth > 0) { + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, underlineWidth, 0xFF5555); + } + } } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 6facd698f..05c3344fe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -196,6 +196,13 @@ private void extractErrors(Token token) { errors.add(fieldAccessInfo.getErrorMessage()); } + // Show assignment errors (type mismatch, final reassignment, etc.) + // Search through FieldInfo's assignments by position + AssignmentInfo assignmentInfo = findAssignmentContainingPosition(token); + if (assignmentInfo != null && assignmentInfo.hasError()) { + errors.add(assignmentInfo.getErrorMessage()); + } + // Show unresolved field errors FieldInfo fieldInfo = token.getFieldInfo(); if (fieldInfo != null && !fieldInfo.isResolved()) { @@ -203,6 +210,38 @@ private void extractErrors(Token token) { } } + /** + * Find an assignment that contains this token's position. + * Searches through all FieldInfo's assignments in the document. + */ + private AssignmentInfo findAssignmentContainingPosition(Token token) { + ScriptLine line = token.getParentLine(); + if (line == null || line.getParent() == null) { + return null; + } + + ScriptDocument doc = line.getParent(); + int tokenStart = token.getGlobalStart(); + + // Search global fields + for (FieldInfo field : doc.getGlobalFields().values()) { + AssignmentInfo assign = field.findAssignmentAtPosition(tokenStart); + if (assign != null) { + return assign; + } + } + + // Search method locals (need access to method locals map) + // For now, use the getAllErroredAssignments approach and filter by position + for (AssignmentInfo assign : doc.getAllErroredAssignments()) { + if (assign.containsPosition(tokenStart)) { + return assign; + } + } + + return null; + } + /** * Find the method call that contains this token's position within its argument list. */ From 455f09ac68d07dfe282898ed4821bf2dcffe1815 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 18:30:06 +0200 Subject: [PATCH 055/337] Captured all external/chained field assignments and their errors for hover info --- .../util/script/interpreter/FieldInfo.java | 20 +++++- .../script/interpreter/ScriptDocument.java | 67 ++++++++++++++++--- .../interpreter/hover/TokenHoverInfo.java | 21 +----- 3 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java index 47eb3191a..7031f21fd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java @@ -161,19 +161,35 @@ public AssignmentInfo getDeclarationAssignment() { /** * Find an assignment that contains the given position. * Returns null if no assignment contains this position. + * Prioritizes LHS matches over RHS matches. */ public AssignmentInfo findAssignmentAtPosition(int position) { // Check declaration assignment first if (declarationAssignment != null && declarationAssignment.containsPosition(position)) { + // Prioritize LHS match + if (declarationAssignment.containsLhsPosition(position)) { + return declarationAssignment; + } + } + + // Check other assignments for LHS matches first (more specific) + for (AssignmentInfo assign : assignments) { + if (assign.containsLhsPosition(position)) { + return assign; + } + } + + // Then check RHS matches (less specific - might just be a reference in the value) + if (declarationAssignment != null && declarationAssignment.containsRhsPosition(position)) { return declarationAssignment; } - // Check other assignments for (AssignmentInfo assign : assignments) { - if (assign.containsPosition(position)) { + if (assign.containsRhsPosition(position)) { return assign; } } + return null; } 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 fdd9b2bf5..63ede56dc 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 @@ -78,6 +78,9 @@ public class ScriptDocument { // Field accesses - stores all parsed field access information private final List fieldAccesses = new ArrayList<>(); + + // Assignments to external fields (fields from reflection, not script-defined) + private final List externalFieldAssignments = new ArrayList<>(); // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); @@ -186,6 +189,7 @@ public void formatCodeText() { methodLocals.clear(); scriptTypes.clear(); methodCalls.clear(); + externalFieldAssignments.clear(); // Phase 1: Find excluded regions (strings/comments) findExcludedRanges(); @@ -2785,6 +2789,7 @@ private void parseAssignments() { field.clearAssignments(); } } + externalFieldAssignments.clear(); // Pattern to find assignments: identifier = expression; // But NOT declarations (Type varName = expr) or compound assignments (+=, -=, etc.) @@ -2959,14 +2964,15 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in } } - // If we couldn't resolve the target field, we can't attach the assignment - if (targetField == null) { - return; - } - // Resolve the source type (RHS) TypeInfo sourceType = resolveExpressionType(rhs, equalsPos + 1); + // Determine if this is a script-defined field or external field + FieldInfo finalTargetField = targetField; + boolean isScriptField = targetField != null && + (globalFields.containsValue(targetField) || + methodLocals.values().stream().anyMatch(m -> m.containsValue(finalTargetField))); + // Create the assignment info using the new constructor AssignmentInfo info = new AssignmentInfo( targetName, @@ -2979,14 +2985,20 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in rhs, receiverType, reflectionField, - targetField.isFinal() + targetField != null ? targetField.isFinal() : false ); // Validate the assignment info.validate(); - // Attach to the FieldInfo - targetField.addAssignment(info); + // Attach to the appropriate location + if (isScriptField && targetField != null) { + // Script-defined field - attach to FieldInfo + targetField.addAssignment(info); + } else { + // External field or unresolved - store separately + externalFieldAssignments.add(info); + } } /** @@ -3368,13 +3380,43 @@ public List getMethodCalls() { public List getFieldAccesses() { return Collections.unmodifiableList(fieldAccesses); } + + /** + * Find an assignment at the given position, prioritizing LHS over RHS. + * Searches across all script fields and external field assignments. + * Used by TokenHoverInfo for finding assignment errors. + */ + public AssignmentInfo findAssignmentAtPosition(int position) { + // Check script fields (FieldInfo.findAssignmentAtPosition already handles LHS/RHS priority) + for (FieldInfo field : globalFields.values()) { + AssignmentInfo assign = field.findAssignmentAtPosition(position); + if (assign != null) { + return assign; + } + } + + // Check external field assignments with same LHS-first priority + for (AssignmentInfo assign : externalFieldAssignments) { + if (assign.containsLhsPosition(position)) { + return assign; + } + } + + for (AssignmentInfo assign : externalFieldAssignments) { + if (assign.containsRhsPosition(position)) { + return assign; + } + } + + return null; + } public Map getGlobalFields() { return Collections.unmodifiableMap(globalFields); } /** - * Get all errored assignments across all fields (global and local). + * Get all errored assignments across all fields (global, local, and external). * Used by ScriptLine to draw error underlines. */ public List getAllErroredAssignments() { @@ -3392,6 +3434,13 @@ public List getAllErroredAssignments() { } } + // Check external field assignments + for (AssignmentInfo assign : externalFieldAssignments) { + if (assign.hasError()) { + errored.add(assign); + } + } + return errored; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 05c3344fe..a55386641 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -212,7 +212,7 @@ private void extractErrors(Token token) { /** * Find an assignment that contains this token's position. - * Searches through all FieldInfo's assignments in the document. + * Searches through all assignments (script fields and external fields). */ private AssignmentInfo findAssignmentContainingPosition(Token token) { ScriptLine line = token.getParentLine(); @@ -223,23 +223,8 @@ private AssignmentInfo findAssignmentContainingPosition(Token token) { ScriptDocument doc = line.getParent(); int tokenStart = token.getGlobalStart(); - // Search global fields - for (FieldInfo field : doc.getGlobalFields().values()) { - AssignmentInfo assign = field.findAssignmentAtPosition(tokenStart); - if (assign != null) { - return assign; - } - } - - // Search method locals (need access to method locals map) - // For now, use the getAllErroredAssignments approach and filter by position - for (AssignmentInfo assign : doc.getAllErroredAssignments()) { - if (assign.containsPosition(tokenStart)) { - return assign; - } - } - - return null; + // Use ScriptDocument's method which handles all prioritization + return doc.findAssignmentAtPosition(tokenStart); } /** From d645ecc99803df917c4cd00c22390253a8808617 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 18:37:34 +0200 Subject: [PATCH 056/337] Removed redundant --- .../gui/util/script/interpreter/ScriptDocument.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 63ede56dc..f653ad770 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 @@ -2882,7 +2882,7 @@ private boolean isVariableDeclaration(String lhs) { String potentialVar = parts[parts.length - 1]; // Type patterns: primitives, or capitalized class names, or generic types - if (isPrimitiveType(potentialType) || + if (TypeResolver.isPrimitiveType(potentialType) || (Character.isUpperCase(potentialType.charAt(0)) && potentialType.matches("[A-Za-z_][A-Za-z0-9_<>\\[\\],\\s]*")) || potentialType.equals("var") || potentialType.equals("let") || potentialType.equals("const")) { @@ -2894,13 +2894,7 @@ private boolean isVariableDeclaration(String lhs) { return false; } - - private boolean isPrimitiveType(String type) { - return type.equals("int") || type.equals("long") || type.equals("short") || type.equals("byte") || - type.equals("float") || type.equals("double") || type.equals("boolean") || type.equals("char") || - type.equals("void"); - } - + /** * Create an AssignmentInfo for a reassignment statement and attach it to the appropriate FieldInfo. */ From 40d35d2f20cdc4b960df1e6a684c99cca5815513 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 18:46:53 +0200 Subject: [PATCH 057/337] Captured Assignment's statement start and made FULL LINE errors --- .../gui/util/script/interpreter/AssignmentInfo.java | 12 +++++++++++- .../gui/util/script/interpreter/ScriptDocument.java | 2 ++ .../gui/util/script/interpreter/ScriptLine.java | 3 +++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java index e748834c2..753adb86e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java @@ -28,6 +28,9 @@ public enum ErrorType { STATIC_CONTEXT_ERROR // Accessing instance field from static context } + // Statement position + private final int statementStart; // Absolute start of statement (includes modifiers/type for declarations) + // Target/LHS info private final String targetName; private final int lhsStart; // Position of first char of target variable/field @@ -53,12 +56,13 @@ public enum ErrorType { private String requiredType; private String providedType; - public AssignmentInfo(String targetName, int lhsStart, int lhsEnd, + public AssignmentInfo(String targetName, int statementStart, int lhsStart, int lhsEnd, TypeInfo targetType, int rhsStart, int rhsEnd, TypeInfo sourceType, String sourceExpr, TypeInfo receiverType, java.lang.reflect.Field reflectionField, boolean isFinal) { this.targetName = targetName; + this.statementStart = statementStart; this.lhsStart = lhsStart; this.lhsEnd = lhsEnd; this.targetType = targetType; @@ -269,12 +273,17 @@ public boolean isLhsError() { * These errors should underline the RHS. */ public boolean isRhsError() { + return false; + } + + public boolean isFullLineError() { return errorType == ErrorType.TYPE_MISMATCH; } // ==================== GETTERS ==================== public String getTargetName() { return targetName; } + public int getStatementStart() { return statementStart; } public int getLhsStart() { return lhsStart; } public int getLhsEnd() { return lhsEnd; } public TypeInfo getTargetType() { return targetType; } @@ -299,6 +308,7 @@ public boolean isRhsError() { public String toString() { return "AssignmentInfo{" + "target='" + targetName + "', " + + "stmt=" + statementStart + ", " + "lhs=[" + lhsStart + "-" + lhsEnd + "], " + "rhs=[" + rhsStart + "-" + rhsEnd + "], " + "targetType=" + targetType + ", " + 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 f653ad770..41fdcf872 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 @@ -2970,6 +2970,7 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in // Create the assignment info using the new constructor AssignmentInfo info = new AssignmentInfo( targetName, + stmtStart, lhsStart, lhsEnd, targetType, @@ -3055,6 +3056,7 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st // Create the assignment info AssignmentInfo info = new AssignmentInfo( varName, + stmtStart, varNameStart, varNameEnd, targetType, diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 09a6159fe..344790af5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -401,6 +401,9 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { underlineStart = assign.getRhsStart(); underlineEnd = assign.getRhsEnd(); underlineText = assign.getSourceExpr(); + } else if (assign.isFullLineError()) { + underlineStart = assign.getStatementStart(); + underlineEnd = assign.getRhsEnd(); } else { // Skip assignments with no displayable error continue; From bacbc877bd5942393a9e6f2f7f9e043c223cd8c7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 18:54:56 +0200 Subject: [PATCH 058/337] Made AssignmentInfo LHS start from statementStart, modifiers and type inclusive --- .../client/gui/util/script/interpreter/AssignmentInfo.java | 4 ++-- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java index 753adb86e..dd7c15f44 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java @@ -238,7 +238,7 @@ private void setError(ErrorType type, String message) { * Check if the given global position falls within the LHS (target) of this assignment. */ public boolean containsLhsPosition(int position) { - return position >= lhsStart && position < lhsEnd; + return position >= statementStart && position < lhsEnd; } /** @@ -252,7 +252,7 @@ public boolean containsRhsPosition(int position) { * Check if the given global position falls anywhere in this assignment (LHS or RHS). */ public boolean containsPosition(int position) { - return position >= lhsStart && position <= rhsEnd; + return position >= statementStart && position <= rhsEnd; } // ==================== ERROR TYPE CHECKS ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index a55386641..dc275cfaf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -127,16 +127,13 @@ public static TokenHoverInfo fromToken(Token token) { break; case LITERAL: - if (info.hasErrors()) - return info; - return null; case KEYWORD: case MODIFIER: case STRING: case COMMENT: - // These don't need hover info typically + if (info.hasErrors()) + return info; return null; - default: // For other types, try to extract any available metadata if (token.getTypeInfo() != null) { From aded6644260b213b9f78a36560babe0fe176f8f7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 18:58:30 +0200 Subject: [PATCH 059/337] Disabled old TYPE_MISMATCH error for fields/methods + their underline rendering, as new AssignmentInfo equivalent replaces these. --- .../client/gui/util/script/interpreter/FieldAccessInfo.java | 4 ++-- .../client/gui/util/script/interpreter/MethodCallInfo.java | 4 ++-- .../npcs/client/gui/util/script/interpreter/ScriptLine.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java index 20cff67ea..1ceefbdeb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java @@ -94,8 +94,8 @@ public void validate() { TypeInfo fieldType = resolvedField.getDeclaredType(); if (fieldType != null && !isTypeCompatible(fieldType, expectedType)) { //extra space is necessary for alignment - setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+ - "\nRequired: " + expectedType.getSimpleName()); + // setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+ + // "\nRequired: " + expectedType.getSimpleName()); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java index 2d7d0afae..cae368d4d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -318,8 +318,8 @@ public void validate() { if (expectedType != null && resolvedMethod != null) { TypeInfo returnType = resolvedMethod.getReturnType(); if (returnType != null && !isTypeCompatible(returnType, expectedType)) { - setError(ErrorType.RETURN_TYPE_MISMATCH, - "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName()); + // setError(ErrorType.RETURN_TYPE_MISMATCH, + // "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName()); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 344790af5..b32749ad5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -309,7 +309,7 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { String beforeMethod = lineText.substring(0, lineLocalStart); int beforeWidth = ClientProxy.Font.width(beforeMethod); int methodWidth = ClientProxy.Font.width(call.getMethodName()); - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); + // drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); } } } @@ -376,7 +376,7 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { String beforeField = lineText.substring(0, lineLocalStart); int beforeWidth = ClientProxy.Font.width(beforeField); int fieldWidth = ClientProxy.Font.width(access.getFieldName()); - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, fieldWidth, 0xFF5555); + // drawCurlyUnderline(lineStartX + beforeWidth, baselineY, fieldWidth, 0xFF5555); } } } From 9e21e4b47f34eae113f1df105228dac4b0bfab0d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 19:31:14 +0200 Subject: [PATCH 060/337] Added error line separator for hover info --- .../interpreter/hover/TokenHoverRenderer.java | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index 7b8105445..4c5cc0c0b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -160,6 +160,12 @@ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, int textX = x + PADDING; int currentY = y + PADDING; int lineHeight = ClientProxy.Font.height(); + + + String packageName = info.getPackageName(); + List declaration = info.getDeclaration(); + List docs = info.getDocumentation(); + List additionalInfo = info.getAdditionalInfo(); // Draw errors first List errors = info.getErrors(); @@ -171,11 +177,18 @@ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, currentY += lineHeight + LINE_SPACING; } } + boolean onlyErrors = errors != null && !errors.isEmpty() && (packageName == null || packageName.isEmpty()) && (declaration == null || + declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty()); + + if (!onlyErrors) { //Add error separator line + currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING - 5; + Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR); + currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING; + } currentY += LINE_SPACING; } // Draw package name - String packageName = info.getPackageName(); if (packageName != null && !packageName.isEmpty()) { String packageText = "\u25CB " + packageName; List wrappedLines = wrapText(packageText, wrapWidth); @@ -186,13 +199,11 @@ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, } // Draw declaration (colored segments with wrapping) - List declaration = info.getDeclaration(); if (!declaration.isEmpty()) { currentY = drawWrappedSegments(textX, currentY, wrapWidth, declaration); currentY += LINE_SPACING; } // Draw documentation - List docs = info.getDocumentation(); if (!docs.isEmpty()) { // Draw separator line before documentation Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR); @@ -207,7 +218,7 @@ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, } // Draw additional info - List additionalInfo = info.getAdditionalInfo(); + if (!additionalInfo.isEmpty()) { currentY += LINE_SPACING; for (String infoLine : additionalInfo) { @@ -374,7 +385,12 @@ private static int calculateContentWidth(TokenHoverInfo info, int maxWidth) { private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) { int lineHeight = ClientProxy.Font.height(); int totalHeight = 0; - + + String packageName = info.getPackageName(); + List declaration = info.getDeclaration(); + List docs = info.getDocumentation(); + List additionalInfo = info.getAdditionalInfo(); + // Errors List errors = info.getErrors(); if (!errors.isEmpty()) { @@ -382,11 +398,17 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) List wrappedLines = wrapText(error, contentWidth); totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING); } + + boolean onlyErrors = errors != null && !errors.isEmpty() && (packageName == null || packageName.isEmpty()) && (declaration == null || + declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty()); + + if (!onlyErrors) //Add error separator height + totalHeight += (SEPARATOR_SPACING + SEPARATOR_HEIGHT) * 2 - 5; + totalHeight += LINE_SPACING; // Extra space after errors } // Package name - String packageName = info.getPackageName(); if (packageName != null && !packageName.isEmpty()) { String packageText = "\u25CB " + packageName; List wrappedLines = wrapText(packageText, contentWidth); @@ -394,14 +416,12 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) } // Declaration (colored segments with wrapping) - List declaration = info.getDeclaration(); if (!declaration.isEmpty()) { totalHeight += calculateSegmentsHeight(contentWidth, declaration); totalHeight += LINE_SPACING; } // Documentation - List docs = info.getDocumentation(); if (!docs.isEmpty()) { // Add space for separator line and spacing totalHeight += SEPARATOR_SPACING + SEPARATOR_HEIGHT; @@ -412,7 +432,6 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) } // Additional info - List additionalInfo = info.getAdditionalInfo(); if (!additionalInfo.isEmpty()) { totalHeight += LINE_SPACING; for (String infoLine : additionalInfo) { From 1786d215add63d90850e8f867833479208135d13 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 20:21:20 +0200 Subject: [PATCH 061/337] Hooked script fields/methods up properly with modifiers and fixed FINAL assignment errors --- .../util/script/interpreter/MethodInfo.java | 37 ++++-- .../script/interpreter/ScriptDocument.java | 120 ++++++++++++++++-- 2 files changed, 137 insertions(+), 20 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 4371fd907..d1f5ef72f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -20,13 +20,13 @@ public final class MethodInfo { private final int bodyEnd; // End of method body (before }) private final boolean resolved; private final boolean isDeclaration; // true if this is a declaration, false if it's a call - private final boolean isStatic; // true if this is a static method + private final int modifiers; // Java Modifier flags (e.g., Modifier.PUBLIC | Modifier.STATIC) private final String documentation; // Javadoc/comment documentation for this method private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, List parameters, int declarationOffset, int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration, - boolean isStatic, String documentation) { + int modifiers, String documentation) { this.name = name; this.returnType = returnType; this.containingType = containingType; @@ -36,24 +36,31 @@ private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, this.bodyEnd = bodyEnd; this.resolved = resolved; this.isDeclaration = isDeclaration; - this.isStatic = isStatic; + this.modifiers = modifiers; this.documentation = documentation; } // Factory methods public static MethodInfo declaration(String name, TypeInfo returnType, List params, int declOffset, int bodyStart, int bodyEnd) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, false, null); + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, 0, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, int declOffset, int bodyStart, int bodyEnd, boolean isStatic) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, isStatic, null); + int modifiers = isStatic ? Modifier.STATIC : 0; + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, modifiers, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, int declOffset, int bodyStart, int bodyEnd, boolean isStatic, String documentation) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, isStatic, documentation); + int modifiers = isStatic ? Modifier.STATIC : 0; + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); + } + + public static MethodInfo declaration(String name, TypeInfo returnType, List params, + int declOffset, int bodyStart, int bodyEnd, int modifiers, String documentation) { + return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); } public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { @@ -63,7 +70,7 @@ public static MethodInfo call(String name, TypeInfo containingType, int paramCou for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false, false, null); + return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false, 0, null); } public static MethodInfo unresolvedCall(String name, int paramCount) { @@ -71,7 +78,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, null, params, -1, -1, -1, false, false, false, null); + return new MethodInfo(name, null, null, params, -1, -1, -1, false, false, 0, null); } /** @@ -81,7 +88,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInfo containingType) { String name = method.getName(); TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); - boolean isStatic = Modifier.isStatic(method.getModifiers()); + int modifiers = method.getModifiers(); List params = new ArrayList<>(); Class[] paramTypes = method.getParameterTypes(); @@ -90,7 +97,7 @@ public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInf params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, isStatic, null); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, modifiers, null); } // Getters @@ -105,7 +112,15 @@ public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInf public boolean isResolved() { return resolved; } public boolean isDeclaration() { return isDeclaration; } public boolean isCall() { return !isDeclaration; } - public boolean isStatic() { return isStatic; } + public int getModifiers() { return modifiers; } + public boolean isStatic() { return Modifier.isStatic(modifiers); } + public boolean isFinal() { return Modifier.isFinal(modifiers); } + public boolean isAbstract() { return Modifier.isAbstract(modifiers); } + public boolean isSynchronized() { return Modifier.isSynchronized(modifiers); } + public boolean isNative() { return Modifier.isNative(modifiers); } + public boolean isPublic() { return Modifier.isPublic(modifiers); } + public boolean isPrivate() { return Modifier.isPrivate(modifiers); } + public boolean isProtected() { return Modifier.isProtected(modifiers); } public String getDocumentation() { return documentation; } /** 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 41fdcf872..d3181d58f 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 @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter; +import net.minecraft.client.Minecraft; import noppes.npcs.client.ClientProxy; import java.util.*; @@ -385,9 +386,13 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { int absPos = bodyStart + 1 + fm.start(); if (isExcluded(absPos)) continue; - String typeName = fm.group(1).trim(); + String typeNameRaw = fm.group(1).trim(); String fieldName = fm.group(2); + // Parse modifiers and strip them + int modifiers = parseModifiers(typeNameRaw); + String typeName = stripModifiers(typeNameRaw); + // Skip if this looks like a method declaration if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || @@ -404,7 +409,7 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { String documentation = extractDocumentationBefore(absPos); TypeInfo fieldType = resolveType(typeName); - FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos, documentation); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos, documentation, -1, -1, modifiers); scriptType.addField(fieldInfo); } @@ -433,12 +438,15 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { // Extract documentation before this method String documentation = extractDocumentationBefore(absPos); + // Extract modifiers by scanning backwards in bodyText from match start + int modifiers = extractModifiersBackwards(mm.start() - 1, bodyText); + TypeInfo returnType = resolveType(returnTypeName); List params = parseParametersWithPositions(paramList, bodyStart + 1 + mm.start(3)); MethodInfo methodInfo = MethodInfo.declaration( - methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd, false, documentation); + methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd, modifiers, documentation); scriptType.addMethod(methodInfo); } } @@ -488,6 +496,9 @@ private void parseMethodDeclarations() { // Extract documentation before this method String documentation = extractDocumentationBefore(m.start()); + // Extract modifiers by scanning backwards from match start + int modifiers = extractModifiersBackwards(m.start() - 1, text); + // Parse parameters with their actual positions List params = parseParametersWithPositions(paramList, m.start(3)); @@ -498,7 +509,7 @@ private void parseMethodDeclarations() { m.start(), bodyStart, bodyEnd, - false, + modifiers, documentation ); methods.add(methodInfo); @@ -578,6 +589,9 @@ private void parseLocalVariables() { continue; } + // Parse modifiers from the raw type declaration + int modifiers = parseModifiers(typeName); + TypeInfo typeInfo; // For var/let/const, infer type from the right-hand side expression @@ -611,7 +625,7 @@ private void parseLocalVariables() { } int declPos = bodyStart + m.start(2); - FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method, initStart, initEnd); + FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method, initStart, initEnd, modifiers); locals.put(varName, fieldInfo); } } @@ -810,6 +824,9 @@ private void parseGlobalFields() { if (isExcluded(position)) continue; + // Parse modifiers from the raw type declaration + int modifiers = parseModifiers(typeNameRaw); + // Strip modifiers (public, private, protected, static, final, etc.) from type name String typeName = stripModifiers(typeNameRaw); @@ -847,7 +864,7 @@ private void parseGlobalFields() { } TypeInfo typeInfo = resolveType(typeName); - FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation, initStart, initEnd); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation, initStart, initEnd, modifiers); globalFields.put(fieldName, fieldInfo); } } @@ -872,6 +889,81 @@ private String stripModifiers(String typeName) { return result.toString(); } + /** + * Parse modifiers from a declaration string and return the corresponding Modifier flags. + * e.g., "public static final" -> Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL + */ + private int parseModifiers(String declaration) { + if (declaration == null) return 0; + + int modifiers = 0; + String[] parts = declaration.trim().split("\\s+"); + for (String part : parts) { + if (part.equals("public")) modifiers |= java.lang.reflect.Modifier.PUBLIC; + else if (part.equals("private")) modifiers |= java.lang.reflect.Modifier.PRIVATE; + else if (part.equals("protected")) modifiers |= java.lang.reflect.Modifier.PROTECTED; + else if (part.equals("static")) modifiers |= java.lang.reflect.Modifier.STATIC; + else if (part.equals("final")) modifiers |= java.lang.reflect.Modifier.FINAL; + else if (part.equals("abstract")) modifiers |= java.lang.reflect.Modifier.ABSTRACT; + else if (part.equals("synchronized")) modifiers |= java.lang.reflect.Modifier.SYNCHRONIZED; + else if (part.equals("volatile")) modifiers |= java.lang.reflect.Modifier.VOLATILE; + else if (part.equals("transient")) modifiers |= java.lang.reflect.Modifier.TRANSIENT; + else if (part.equals("native")) modifiers |= java.lang.reflect.Modifier.NATIVE; + else if (part.equals("strictfp")) modifiers |= java.lang.reflect.Modifier.STRICT; + } + return modifiers; + } + + /** + * Scan backwards from scanStart position in the given text to extract modifier keywords. + * Returns the parsed modifier flags. + * + * @param scanStart The position to start scanning backwards from (exclusive) + * @param sourceText The text to scan in + * @return The combined modifier flags + */ + private int extractModifiersBackwards(int scanStart, String sourceText) { + // Skip whitespace + while (scanStart >= 0 && Character.isWhitespace(sourceText.charAt(scanStart))) { + scanStart--; + } + + // Scan backwards collecting modifier words + StringBuilder modifiersText = new StringBuilder(); + while (scanStart >= 0) { + // Skip whitespace + while (scanStart >= 0 && Character.isWhitespace(sourceText.charAt(scanStart))) { + scanStart--; + } + if (scanStart < 0) break; + + // Read a word backwards + int wordEnd = scanStart + 1; + while (scanStart >= 0 && Character.isJavaIdentifierPart(sourceText.charAt(scanStart))) { + scanStart--; + } + int wordStart = scanStart + 1; + if (wordStart < wordEnd) { + String word = sourceText.substring(wordStart, wordEnd); + // Check if it's a modifier + if (TypeResolver.isModifier(word)) { + if (modifiersText.length() > 0) { + modifiersText.insert(0, " "); + } + modifiersText.insert(0, word); + } else { + // Hit a non-modifier word, stop scanning + break; + } + } else { + break; + } + } + + // Parse the collected modifiers + return parseModifiers(modifiersText.toString()); + } + /** * Extract documentation comment immediately preceding a position. * Supports both single-line (//) and multi-line (/* *\/) comment styles. @@ -2949,6 +3041,8 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in reflectionField = targetField.getReflectionField(); } } + // Minecraft.getMinecraft().thePlayer.PERSISTED_NBT_TAG = null; + } else { // Simple variable targetField = resolveVariable(targetName, lhsStart); @@ -2957,7 +3051,7 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in reflectionField = targetField.getReflectionField(); } } - + // Resolve the source type (RHS) TypeInfo sourceType = resolveExpressionType(rhs, equalsPos + 1); @@ -2967,6 +3061,13 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in (globalFields.containsValue(targetField) || methodLocals.values().stream().anyMatch(m -> m.containsValue(finalTargetField))); + // Determine if the target field is final + // For script fields, use the modifiers; for external fields, use reflection + boolean isFinal = false; + if (targetField != null) { + isFinal = targetField.isFinal(); + } + // Create the assignment info using the new constructor AssignmentInfo info = new AssignmentInfo( targetName, @@ -2980,7 +3081,7 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in rhs, receiverType, reflectionField, - targetField != null ? targetField.isFinal() : false + isFinal ); // Validate the assignment @@ -3054,6 +3155,7 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st TypeInfo sourceType = resolveExpressionType(rhs, rhsStart); // Create the assignment info + // Declaration assignments should NOT check final status - this is the one place where final fields can be assigned AssignmentInfo info = new AssignmentInfo( varName, stmtStart, @@ -3066,7 +3168,7 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st rhs, null, // No receiver for simple declarations targetField.getReflectionField(), - targetField.isFinal() + false // Don't flag as final for declaration assignments - initial assignment is always allowed ); // Validate the assignment From 17e0782a3248693401990b078f99a6d88c219d9c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 20:46:40 +0200 Subject: [PATCH 062/337] Forgot to check for assignments in method locals --- .../gui/util/script/interpreter/ScriptDocument.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 d3181d58f..bbc844606 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 @@ -3492,6 +3492,15 @@ public AssignmentInfo findAssignmentAtPosition(int position) { return assign; } } + + for (Map locals : methodLocals.values()) { + for (FieldInfo field : locals.values()) { + AssignmentInfo assign = field.findAssignmentAtPosition(position); + if (assign != null) { + return assign; + } + } + } // Check external field assignments with same LHS-first priority for (AssignmentInfo assign : externalFieldAssignments) { From 794ff0c9b05852c02d5c96f2f0679b1715c7cca0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 20:52:34 +0200 Subject: [PATCH 063/337] Removed whitespaces from AssingmentInfo's statementStart --- .../client/gui/util/script/interpreter/ScriptDocument.java | 5 +++++ 1 file changed, 5 insertions(+) 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 bbc844606..9201ca4e4 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 @@ -2924,6 +2924,11 @@ private void parseAssignments() { } if (stmtStart < 0) stmtStart = 0; + // Skip leading whitespace to get to the actual first character of the statement + while (stmtStart < equalsPos && Character.isWhitespace(text.charAt(stmtStart))) { + stmtStart++; + } + // Find the end of this statement (next ;) int stmtEnd = text.indexOf(';', equalsPos); if (stmtEnd < 0) stmtEnd = text.length(); From a4eb63917f6180a256ab231fa972e145f0dec303 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 22:00:03 +0200 Subject: [PATCH 064/337] Proper hover info formatting for declaration --- .../interpreter/hover/TokenHoverInfo.java | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index dc275cfaf..08623ebaf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -796,13 +796,83 @@ private void addInitializationTokens(Token token, FieldInfo fieldInfo) { // Add space before '=' for readability addSegment(" ", TokenType.DEFAULT.getHexColor()); - - // Add each token with its proper color + + // Add each token with its proper color, ensuring normalized spacing between tokens + String lastText = null; for (Token initToken : initTokens) { - addSegment(initToken.getText(), initToken.getType().getHexColor()); + String text = initToken.getText(); + + // Normalize whitespace - replace all newlines and multiple spaces with single space + text = text.replaceAll("\\s+", " ").trim(); + + // Skip if token became empty after whitespace removal + if (text.isEmpty()) { + continue; + } + + // Determine if we need a space between last token and current token + if (lastText != null && shouldAddSpace(lastText, text)) { + addSegment(" ", TokenType.DEFAULT.getHexColor()); + } + + addSegment(text, initToken.getType().getHexColor()); + lastText = text; } } + /** + * Determine if a space should be added between two tokens. + */ + private boolean shouldAddSpace(String lastToken, String currentToken) { + if (lastToken.isEmpty() || currentToken.isEmpty()) + return false; + + char lastChar = lastToken.charAt(lastToken.length() - 1); + char firstChar = currentToken.charAt(0); + + // Never add space before these closing/trailing characters + if (firstChar == '(' || firstChar == '[' || firstChar == '{' || + firstChar == '.' || firstChar == ',' || firstChar == ';' || + (firstChar == ':' && lastToken.equals("?"))) { + return false; + } + + // Never add space after these opening/leading characters + if (lastChar == '(' || lastChar == '[' || lastChar == '{' || lastChar == '.') { + return false; + } + + // Add space around operators (check last/first characters) + if (isOperatorChar(lastChar) || isOperatorChar(firstChar)) { + return true; + } + + // Add space after closing brackets/parens before other tokens + if (lastChar == ')' || lastChar == ']' || lastChar == '}') { + return true; + } + + // Add space after colons and commas (except after ?: ternary) + if ((lastChar == ':' && !lastToken.equals("?:")) || lastChar == ',') { + return true; + } + + // Add space between identifiers/keywords and numbers + boolean lastIsIdentifier = Character.isLetterOrDigit(lastChar) || lastChar == '_'; + boolean firstIsIdentifier = Character.isLetterOrDigit(firstChar) || firstChar == '_'; + if (lastIsIdentifier && firstIsIdentifier) { + return true; + } + + return false; + } + + /** + * Check if a character is an operator that needs spacing. + */ + private boolean isOperatorChar(char c) { + return "+-*/%<>=!&|^?:".indexOf(c) >= 0; + } // ==================== GETTERS ==================== public String getPackageName() { return packageName; } From 6400c4a4e84ad4652a389ef8302d4c8c0b421791 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 22:06:33 +0200 Subject: [PATCH 065/337] Fixed declaration hover info being not rendered for resolved method calls --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 08623ebaf..fba3f9b43 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -879,7 +879,7 @@ private boolean isOperatorChar(char c) { public String getIconIndicator() { return iconIndicator; } public List getDeclaration() { - if (getErrors().stream().anyMatch(err -> err.contains("Cannot resolve"))) + if (getErrors().stream().anyMatch(err -> err.contains("Cannot resolve symbol"))) return new ArrayList<>(); return declaration; } From 3b2ac768c646dbb9d4d1f6fe1ce803657180afc8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 22:44:41 +0200 Subject: [PATCH 066/337] Error "var already defined in scope" --- .../script/interpreter/AssignmentInfo.java | 31 ++++++++++++-- .../script/interpreter/ScriptDocument.java | 40 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java index dd7c15f44..d6c5e0995 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java @@ -25,7 +25,8 @@ public enum ErrorType { PRIVATE_ACCESS, // Attempting to access private field PROTECTED_ACCESS, // Attempting to access protected field from invalid context UNRESOLVED_TARGET, // Target variable/field doesn't exist - STATIC_CONTEXT_ERROR // Accessing instance field from static context + STATIC_CONTEXT_ERROR, // Accessing instance field from static context + DUPLICATE_DECLARATION // Variable is already defined in the scope } // Statement position @@ -74,6 +75,29 @@ public AssignmentInfo(String targetName, int statementStart, int lhsStart, int l this.reflectionField = reflectionField; this.isFinal = isFinal; } + + /** + * Factory method to create an AssignmentInfo representing a duplicate declaration error. + * Only the LHS (variable name) position is relevant for underlining. + */ + public static AssignmentInfo duplicateDeclaration(String varName, int nameStart, int nameEnd, String errorMessage) { + AssignmentInfo info = new AssignmentInfo( + varName, + nameStart, // statementStart = nameStart for underline positioning + nameStart, // lhsStart + nameEnd, // lhsEnd + null, // targetType + -1, // rhsStart (not applicable) + -1, // rhsEnd (not applicable) + null, // sourceType + null, // sourceExpr + null, // receiverType + null, // reflectionField + false // isFinal + ); + info.setError(ErrorType.DUPLICATE_DECLARATION, errorMessage); + return info; + } // ==================== VALIDATION ==================== @@ -258,14 +282,15 @@ public boolean containsPosition(int position) { // ==================== ERROR TYPE CHECKS ==================== /** - * Check if this is an LHS error (final reassignment, access errors). + * Check if this is an LHS error (final reassignment, access errors, duplicate declarations). * These errors should underline the LHS. */ public boolean isLhsError() { return errorType == ErrorType.FINAL_REASSIGNMENT || errorType == ErrorType.PRIVATE_ACCESS || errorType == ErrorType.PROTECTED_ACCESS || - errorType == ErrorType.STATIC_CONTEXT_ERROR; + errorType == ErrorType.STATIC_CONTEXT_ERROR || + errorType == ErrorType.DUPLICATE_DECLARATION; } /** 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 9201ca4e4..f44f8c9d6 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 @@ -83,6 +83,9 @@ public class ScriptDocument { // Assignments to external fields (fields from reflection, not script-defined) private final List externalFieldAssignments = new ArrayList<>(); + // Declaration errors (duplicate declarations, etc.) + private final List declarationErrors = new ArrayList<>(); + // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); @@ -191,6 +194,7 @@ public void formatCodeText() { scriptTypes.clear(); methodCalls.clear(); externalFieldAssignments.clear(); + declarationErrors.clear(); // Phase 1: Find excluded regions (strings/comments) findExcludedRanges(); @@ -626,7 +630,16 @@ private void parseLocalVariables() { int declPos = bodyStart + m.start(2); FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method, initStart, initEnd, modifiers); - locals.put(varName, fieldInfo); + + // Check for duplicate declaration (in locals or globals) + if (locals.containsKey(varName) || globalFields.containsKey(varName)) { + // Create a declaration error for this duplicate + AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( + varName, declPos, declPos + varName.length(), + "Variable '" + varName + "' is already defined in the scope"); + declarationErrors.add(dupError); + } else if (!locals.containsKey(varName)) + locals.put(varName, fieldInfo); } } } @@ -865,7 +878,19 @@ private void parseGlobalFields() { TypeInfo typeInfo = resolveType(typeName); FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation, initStart, initEnd, modifiers); - globalFields.put(fieldName, fieldInfo); + + // Check for duplicate declaration + if (globalFields.containsKey(fieldName)) { + // Create a declaration error for this duplicate + AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( + fieldName, position, position + fieldName.length(), + "Variable '" + fieldName + "' is already defined in the scope"); + declarationErrors.add(dupError); + } else { + globalFields.put(fieldName, fieldInfo); + } + + } } } @@ -3152,7 +3177,7 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st // Resolve the target field (should already exist from parseGlobalFields or parseLocalVariables) FieldInfo targetField = resolveVariable(varName, varNameStart); - if (targetField == null) { + if (targetField == null || targetField.getDeclarationAssignment() != null) { return; // Field doesn't exist, can't attach assignment } @@ -3506,6 +3531,12 @@ public AssignmentInfo findAssignmentAtPosition(int position) { } } } + + for (AssignmentInfo assign : declarationErrors) { + if (assign.containsLhsPosition(position)) { + return assign; + } + } // Check external field assignments with same LHS-first priority for (AssignmentInfo assign : externalFieldAssignments) { @@ -3552,6 +3583,9 @@ public List getAllErroredAssignments() { errored.add(assign); } } + + // Include declaration errors (duplicates, etc.) + errored.addAll(declarationErrors); return errored; } From 643b5e0d70364d8db7c653a76e7d044cbe2c48e3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 22:49:40 +0200 Subject: [PATCH 067/337] Fixed hover info being wrapped to the wrong font width --- .../script/interpreter/hover/TokenHoverRenderer.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index 4c5cc0c0b..da269737b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -280,15 +280,7 @@ private static List wrapText(String text, int maxWidth) { if (text == null || text.isEmpty()) { return lines; } - - // Use Minecraft's built-in wrapping if available - Minecraft mc = Minecraft.getMinecraft(); - if (mc.fontRenderer != null) { - @SuppressWarnings("unchecked") - List wrapped = mc.fontRenderer.listFormattedStringToWidth(text, maxWidth); - return wrapped; - } - + // Fallback: simple word wrapping String[] words = text.split(" "); StringBuilder currentLine = new StringBuilder(); From 992aea663ba16def56ae27e4711d8de9ea4d61a5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 27 Dec 2025 23:26:09 +0200 Subject: [PATCH 068/337] Aligned spacings for TYPE_MISMATCH errors --- .../client/gui/util/script/interpreter/AssignmentInfo.java | 2 +- .../client/gui/util/script/interpreter/FieldAccessInfo.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java index d6c5e0995..35bcdadae 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java @@ -144,7 +144,7 @@ public void validate() { * Build a formatted type mismatch error message (IntelliJ style). */ private String buildTypeMismatchMessage() { - return "Required type: " + requiredType + "\nProvided: " + providedType; + return "Provided type: " + providedType + "\nRequired: " + requiredType; } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java index 1ceefbdeb..ffd07b045 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java @@ -94,8 +94,8 @@ public void validate() { TypeInfo fieldType = resolvedField.getDeclaredType(); if (fieldType != null && !isTypeCompatible(fieldType, expectedType)) { //extra space is necessary for alignment - // setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+ - // "\nRequired: " + expectedType.getSimpleName()); + //setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+ + //"\nRequired: " + expectedType.getSimpleName()); } } } From dfc0232e995e32c674e53eaa75ca0d7bb678b001 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 00:19:00 +0200 Subject: [PATCH 069/337] Got classes defined in script fully functional --- .../script/interpreter/ScriptDocument.java | 70 ++++++++++++++----- .../script/interpreter/ScriptTypeInfo.java | 13 ++-- .../interpreter/hover/TokenHoverInfo.java | 38 ++++++++++ 3 files changed, 100 insertions(+), 21 deletions(-) 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 f44f8c9d6..b3b6bd21e 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 @@ -336,9 +336,11 @@ private void parseStructure() { * Creates ScriptTypeInfo instances and stores them for later resolution. */ private void parseScriptTypes() { - // Pattern: (class|interface|enum) ClassName { ... } + // Pattern: [modifiers] (class|interface|enum) ClassName [optional ()] { ... } + // Matches optional modifiers (public, private, static, final, abstract) before class/interface/enum + // Also allows optional () after the class name (common mistake) Pattern typeDecl = Pattern.compile( - "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:extends\\s+[A-Za-z_][a-zA-Z0-9_.]*)?\\s*(?:implements\\s+[A-Za-z_][a-zA-Z0-9_.,\\s]*)?\\s*\\{"); + "(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:\\(\\))?\\s*(?:extends\\s+[A-Za-z_][a-zA-Z0-9_.]*)?\\s*(?:implements\\s+[A-Za-z_][a-zA-Z0-9_.,\\s]*)?\\s*\\{"); Matcher m = typeDecl.matcher(text); while (m.find()) { @@ -361,8 +363,12 @@ private void parseScriptTypes() { default: kind = TypeInfo.Kind.CLASS; break; } + // Extract modifiers from the matched text + String fullMatch = text.substring(m.start(), m.end()); + int modifiers = parseModifiers(fullMatch); + ScriptTypeInfo scriptType = ScriptTypeInfo.create( - typeName, kind, m.start(), bodyStart, bodyEnd); + typeName, kind, m.start(), bodyStart, bodyEnd, modifiers); // Parse fields and methods inside this type parseScriptTypeMembers(scriptType); @@ -1697,22 +1703,30 @@ private void markMethodCalls(List marks) { } else { if (isScriptMethod(methodName)) { resolvedMethod = getScriptMethodInfo(methodName); - MethodCallInfo callInfo = new MethodCallInfo( - methodName, nameStart, nameEnd, openParen, closeParen, - arguments, null, resolvedMethod - ); - // Only set expected type if this is the final expression (not followed by .field or .method) - if (!isFollowedByDot(closeParen)) { - TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); - if (expectedType != null) { - callInfo.setExpectedType(expectedType); + // Check if this is a method from a script type + // Instance methods from script types cannot be called without a receiver + if (resolvedMethod != null && isMethodFromScriptType(resolvedMethod) && !resolvedMethod.isStatic()) { + // Instance method called without receiver - mark as undefined + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); + } else { + MethodCallInfo callInfo = new MethodCallInfo( + methodName, nameStart, nameEnd, openParen, closeParen, + arguments, null, resolvedMethod + ); + + // Only set expected type if this is the final expression (not followed by .field or .method) + if (!isFollowedByDot(closeParen)) { + TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); + if (expectedType != null) { + callInfo.setExpectedType(expectedType); + } } + + callInfo.validate(); + methodCalls.add(callInfo); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } - - callInfo.validate(); - methodCalls.add(callInfo); - marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } @@ -2476,6 +2490,30 @@ private MethodInfo getScriptMethodInfo(String methodName) { } return null; } + + /** + * Check if a method belongs to a script-defined type (class/interface/enum). + * Returns true if the method is defined inside a script type. + */ + private boolean isMethodFromScriptType(MethodInfo method) { + if (method == null || !method.isDeclaration()) { + return false; + } + + int declPos = method.getDeclarationOffset(); + if (declPos < 0) { + return false; + } + + // Check if the method's declaration position is inside any script type's body + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + if (scriptType.containsPosition(declPos)) { + return true; + } + } + + return false; + } private void markVariables(List marks) { Set knownKeywords = new HashSet<>(Arrays.asList( diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java index b0ab251a8..7c7f47619 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java @@ -18,6 +18,7 @@ public class ScriptTypeInfo extends TypeInfo { private final int declarationOffset; private final int bodyStart; private final int bodyEnd; + private final int modifiers; // Java reflection modifiers (public, static, final, etc.) // Script-defined members private final Map fields = new HashMap<>(); @@ -28,24 +29,25 @@ public class ScriptTypeInfo extends TypeInfo { private ScriptTypeInfo outerClass; private ScriptTypeInfo(String simpleName, String fullName, Kind kind, - int declarationOffset, int bodyStart, int bodyEnd) { + int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { super(simpleName, fullName, "", kind, null, true, null, true); this.scriptClassName = simpleName; this.declarationOffset = declarationOffset; this.bodyStart = bodyStart; this.bodyEnd = bodyEnd; + this.modifiers = modifiers; } // Factory method public static ScriptTypeInfo create(String simpleName, Kind kind, - int declarationOffset, int bodyStart, int bodyEnd) { - return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd); + int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { + return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd, modifiers); } public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTypeInfo outer, - int declarationOffset, int bodyStart, int bodyEnd) { + int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { String fullName = outer.getFullName() + "$" + simpleName; - ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd); + ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd, modifiers); inner.outerClass = outer; outer.innerClasses.add(inner); return inner; @@ -56,6 +58,7 @@ public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTyp public int getDeclarationOffset() { return declarationOffset; } public int getBodyStart() { return bodyStart; } public int getBodyEnd() { return bodyEnd; } + public int getModifiers() { return modifiers; } public ScriptTypeInfo getOuterClass() { return outerClass; } public List getInnerClasses() { return innerClasses; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index fba3f9b43..6fa3efc93 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -351,6 +351,44 @@ private void extractClassInfo(Token token) { addSegment(", ...", TokenType.DEFAULT.getHexColor()); } } + } else if (typeInfo instanceof ScriptTypeInfo) { + // Script-defined type + ScriptTypeInfo scriptType = (ScriptTypeInfo) typeInfo; + + // Icon + if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) { + iconIndicator = "I"; + } else if (scriptType.getKind() == TypeInfo.Kind.ENUM) { + iconIndicator = "E"; + } else { + iconIndicator = "C"; + } + + // Build declaration with modifiers + int mods = scriptType.getModifiers(); + + // Modifiers + if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isAbstract(mods) && scriptType.getKind() != TypeInfo.Kind.INTERFACE) + addSegment("abstract ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isFinal(mods)) addSegment("final ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isStatic(mods)) addSegment("static ", TokenType.MODIFIER.getHexColor()); + + // Class type keyword + if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) { + addSegment("interface ", TokenType.MODIFIER.getHexColor()); + } else if (scriptType.getKind() == TypeInfo.Kind.ENUM) { + addSegment("enum ", TokenType.MODIFIER.getHexColor()); + } else { + addSegment("class ", TokenType.MODIFIER.getHexColor()); + } + + // Class name + int classColor = scriptType.getKind() == TypeInfo.Kind.INTERFACE ? TokenType.INTERFACE_DECL.getHexColor() + : scriptType.getKind() == TypeInfo.Kind.ENUM ? TokenType.ENUM_DECL.getHexColor() + : TokenType.IMPORTED_CLASS.getHexColor(); + addSegment(typeInfo.getSimpleName(), classColor); + } else { // Unresolved type iconIndicator = "?"; From ce5a67eaa2a956c957eae62a00682454b663e912 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 01:07:21 +0200 Subject: [PATCH 070/337] Added constructors to script class types and hooked up their hover infos --- .../script/interpreter/ScriptDocument.java | 112 +++++++++++++++++- .../util/script/interpreter/ScriptLine.java | 4 + .../script/interpreter/ScriptTypeInfo.java | 31 +++++ .../gui/util/script/interpreter/TypeInfo.java | 27 +++++ .../script/interpreter/hover/HoverState.java | 2 +- .../interpreter/hover/TokenHoverInfo.java | 44 ++++++- 6 files changed, 217 insertions(+), 3 deletions(-) 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 b3b6bd21e..62a53d1ad 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 @@ -459,6 +459,45 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd, modifiers, documentation); scriptType.addMethod(methodInfo); } + + // Parse constructors (ClassName(params) { ... }) + // Constructor pattern: ClassName matches the script type name, no return type + String typeName = scriptType.getSimpleName(); + Pattern constructorPattern = Pattern.compile( + "\\b" + Pattern.quote(typeName) + "\\s*\\(([^)]*)\\)\\s*\\{"); + Matcher cm = constructorPattern.matcher(bodyText); + while (cm.find()) { + int absPos = bodyStart + 1 + cm.start(); + if (isExcluded(absPos)) continue; + + String paramList = cm.group(1); + + int constructorBodyStart = bodyStart + 1 + cm.end() - 1; + int constructorBodyEnd = findMatchingBrace(constructorBodyStart); + if (constructorBodyEnd < 0) constructorBodyEnd = bodyEnd; + + // Extract documentation before this constructor + String documentation = extractDocumentationBefore(absPos); + + // Extract modifiers by scanning backwards + int modifiers = extractModifiersBackwards(cm.start() - 1, bodyText); + + // Parse parameters with their actual positions + List params = parseParametersWithPositions(paramList, bodyStart + 1 + cm.start(1)); + + // Constructors don't have a return type, but we'll use the containing type as a marker + MethodInfo constructorInfo = MethodInfo.declaration( + typeName, + scriptType, // Return type is the type itself + params, + absPos, + constructorBodyStart, + constructorBodyEnd, + modifiers, + documentation + ); + scriptType.addConstructor(constructorInfo); + } } /** @@ -1202,7 +1241,6 @@ private List buildMarks() { // Type declarations and usages markTypeDeclarations(marks); - addPatternMarks(marks, NEW_TYPE_PATTERN, TokenType.NEW_TYPE, 1); // Methods markMethodDeclarations(marks); @@ -1534,6 +1572,19 @@ else if (c == '>') } } + /** + * Wrapper class to hold both TypeInfo and constructor MethodInfo for "new" expressions. + */ + static class NewExpressionInfo { + final TypeInfo typeInfo; + final MethodInfo constructor; + + NewExpressionInfo(TypeInfo typeInfo, MethodInfo constructor) { + this.typeInfo = typeInfo; + this.constructor = constructor; + } + } + /** * Recursively parse and mark generic type parameters. * Handles arbitrarily nested generics like Map>>. @@ -2534,6 +2585,19 @@ private void markVariables(List marks) { marks.add(new ScriptLine.Mark(pos, pos + name.length(), TokenType.PARAMETER, param)); } } + + // Mark constructor parameters from script types + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + for (MethodInfo constructor : scriptType.getConstructors()) { + for (FieldInfo param : constructor.getParameters()) { + int pos = param.getDeclarationOffset(); + if (pos >= 0) { + String name = param.getName(); + marks.add(new ScriptLine.Mark(pos, pos + name.length(), TokenType.PARAMETER, param)); + } + } + } + } Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); @@ -3279,6 +3343,7 @@ private void markImportedClassUsages(List marks) { while (tm.find()) { String className = tm.group(2); + String newKeyword = tm.group(1); int start = tm.start(2); int end = tm.end(2); @@ -3292,6 +3357,51 @@ private void markImportedClassUsages(List marks) { // Try to resolve the class TypeInfo info = resolveType(className); if (info != null && info.isResolved()) { + boolean isNewCreation = newKeyword != null && newKeyword.trim().equals("new"); + boolean isConstructorDecl = info instanceof ScriptTypeInfo && className.equals(info.getSimpleName()); + + // Check if this is a "new" expression or constructor declaration + if (isNewCreation || isConstructorDecl) { + // Find opening paren after the class name + int searchPos = end; + while (searchPos < text.length() && Character.isWhitespace(text.charAt(searchPos))) + searchPos++; + + if (searchPos < text.length() && text.charAt(searchPos) == '(') { + int openParen = searchPos; + int closeParen = findMatchingParen(openParen); + + if (closeParen >= 0) { + // For constructor declarations, verify it's followed by opening brace + if (isConstructorDecl && !isNewCreation) { + int braceSearch = closeParen + 1; + while (braceSearch < text.length() && Character.isWhitespace(text.charAt(braceSearch))) + braceSearch++; + + if (braceSearch >= text.length() || text.charAt(braceSearch) != '{') { + // Not a constructor declaration, treat as normal type usage + marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), info)); + continue; + } + } + + // Parse arguments to find matching constructor + List arguments = parseMethodArguments(openParen + 1, closeParen); + int argCount = arguments.size(); + + if (info.hasConstructors()) { + MethodInfo constructor = info.findConstructor(argCount); + if (constructor != null) { + // Attach both type info and constructor + NewExpressionInfo newExpr = new NewExpressionInfo(info, constructor); + marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), newExpr)); + continue; + } + } + } + } + } + marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), info)); } else { // Unknown type - mark as undefined diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index b32749ad5..4fd6d5cdc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -152,6 +152,10 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume if (mark.metadata != null) { if (mark.metadata instanceof TypeInfo) { token.setTypeInfo((TypeInfo) mark.metadata); + } else if (mark.metadata instanceof ScriptDocument.NewExpressionInfo) { + ScriptDocument.NewExpressionInfo newExpr = (ScriptDocument.NewExpressionInfo) mark.metadata; + token.setTypeInfo(newExpr.typeInfo); + token.setMethodInfo(newExpr.constructor); } else if (mark.metadata instanceof FieldInfo.ArgInfo) { FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) mark.metadata; token.setFieldInfo(ctx.fieldInfo); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java index 7c7f47619..a2fba036d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java @@ -23,6 +23,7 @@ public class ScriptTypeInfo extends TypeInfo { // Script-defined members private final Map fields = new HashMap<>(); private final Map> methods = new HashMap<>(); // name -> list of overloads + private final List constructors = new ArrayList<>(); // List of constructors private final List innerClasses = new ArrayList<>(); // Parent class reference (for inner class resolution) @@ -143,6 +144,36 @@ public Map> getMethods() { return new HashMap<>(methods); } + // ==================== CONSTRUCTOR MANAGEMENT ==================== + + public void addConstructor(MethodInfo constructor) { + constructors.add(constructor); + } + + @Override + public List getConstructors() { + return new ArrayList<>(constructors); + } + + @Override + public boolean hasConstructors() { + return !constructors.isEmpty(); + } + + /** + * Find the best matching constructor for the given argument count. + * Returns null if no constructor matches. + */ + @Override + public MethodInfo findConstructor(int argCount) { + for (MethodInfo constructor : constructors) { + if (constructor.getParameterCount() == argCount) { + return constructor; + } + } + return null; + } + // ==================== INNER CLASS LOOKUP ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java index 862e5f108..2184f42a6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -1,5 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter; +import java.util.ArrayList; +import java.util.List; + /** * Represents resolved type information for a class/interface/enum. * Base class for type metadata. Extended by ScriptTypeInfo for script-defined types. @@ -175,6 +178,30 @@ public boolean hasMethod(String methodName, int paramCount) { } return false; } + + /** + * Check if this type has constructors. + * Override in ScriptTypeInfo for script-defined types. + */ + public boolean hasConstructors() { + return false; + } + + /** + * Get the constructors for this type. + * Override in ScriptTypeInfo for script-defined types. + */ + public List getConstructors() { + return new ArrayList<>(); + } + + /** + * Find the best matching constructor for the given argument count. + * Override in ScriptTypeInfo for script-defined types. + */ + public MethodInfo findConstructor(int argCount) { + return null; + } /** * Check if this type has a field with the given name. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java index 65e9d7c2d..4025e0611 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java @@ -151,7 +151,7 @@ public void update(int mouseX, int mouseY, ScriptDocument document, public void update(int mouseX, int mouseY, Token token, int tokenX, int tokenY, int tokenW) { lastMouseX = mouseX; lastMouseY = mouseY; - clickToPinEnabled=false; + clickToPinEnabled=true; // If a token has been pinned by click, ignore mouse movement updates if (pinnedToken != null) { // keep pinned tooltip visible diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 6fa3efc93..8a0c6c1e3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -397,6 +397,14 @@ private void extractClassInfo(Token token) { errors.add("Cannot resolve class '" + typeInfo.getSimpleName() + "'"); } } + + // If this is a NEW_TYPE token with constructor info, show the constructor signature + if (token.getMethodInfo() != null) { + MethodInfo constructor = token.getMethodInfo(); + declaration.add(new TextSegment("\n", TokenType.DEFAULT.getHexColor())); + additionalInfo.add("Constructor"); + buildConstructorDeclaration(constructor, typeInfo); + } } private void extractMethodCallInfo(Token token) { @@ -607,7 +615,41 @@ private void extractFieldInfoGeneric(Token token) { } } - // ==================== HELPER METHODS ==================== + private void buildConstructorDeclaration(MethodInfo constructor, TypeInfo containingType) { + // Modifiers (public, private, etc.) + declaration.clear(); + int mods = constructor.getModifiers(); + if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor()); + else if (Modifier.isProtected(mods)) addSegment("protected ", TokenType.MODIFIER.getHexColor()); + else if (Modifier.isPrivate(mods)) addSegment("private ", TokenType.MODIFIER.getHexColor()); + + // Constructor name (same as class name) + addSegment(constructor.getName(), containingType.getTokenType().getHexColor()); + + // Parameters + addSegment("(", TokenType.DEFAULT.getHexColor()); + List params = constructor.getParameters(); + for (int i = 0; i < params.size(); i++) { + if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); + FieldInfo param = params.get(i); + TypeInfo paramType = param.getDeclaredType(); + if (paramType != null) { + int paramTypeColor = getColorForTypeInfo(paramType); + addSegment(paramType.getSimpleName(), paramTypeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); + } + addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); + } + addSegment(")", TokenType.DEFAULT.getHexColor()); + + // Add documentation if available + if (constructor.getDocumentation() != null && !constructor.getDocumentation().isEmpty()) { + String[] docLines = constructor.getDocumentation().split("\n"); + for (String line : docLines) { + documentation.add(line); + } + } + } private void addSegment(String text, int color) { declaration.add(new TextSegment(text, color)); From c39897702297794c5c5248cf20f0ca5ba6dbb6ea Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 15:49:22 +0200 Subject: [PATCH 071/337] Method declarations skip names after "new" keyword --- .../npcs/client/gui/util/script/interpreter/ScriptDocument.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 62a53d1ad..0ec1b8502 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 @@ -1660,7 +1660,7 @@ private void markMethodDeclarations(List marks) { // Skip class/interface/enum declarations - these look like method declarations // but "class Foo(" is not a method, it's a class declaration String returnType = m.group(1); - if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum")) { + if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum") ||returnType.equals("new")) { continue; } From a5ab3cb443bae001aa06b63fa21d63c44f26552b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 15:49:41 +0200 Subject: [PATCH 072/337] Removed the NEW_TYPE token, no longer needed --- .../client/gui/util/script/interpreter/ScriptTextContainer.java | 1 - .../noppes/npcs/client/gui/util/script/interpreter/Token.java | 2 +- .../npcs/client/gui/util/script/interpreter/TokenType.java | 2 -- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) 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 9e3f83b96..5c73fe530 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 @@ -128,7 +128,6 @@ private JavaTextContainer.TokenType toLegacyTokenType(noppes.npcs.client.gui.uti if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.IMPORT_KEYWORD) return JavaTextContainer.TokenType.IMPORT_KEYWORD; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.KEYWORD) return JavaTextContainer.TokenType.KEYWORD; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.MODIFIER) return JavaTextContainer.TokenType.MODIFIER; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.NEW_TYPE) return JavaTextContainer.TokenType.NEW_TYPE; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.INTERFACE_DECL) return JavaTextContainer.TokenType.INTERFACE_DECL; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.ENUM_DECL) return JavaTextContainer.TokenType.ENUM_DECL; if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_DECL) return JavaTextContainer.TokenType.CLASS_DECL; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java index 06aa821a7..af4af8ac7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java @@ -191,7 +191,7 @@ public boolean isKeyword() { public boolean isTypeReference() { return type == TokenType.TYPE_DECL || type == TokenType.IMPORTED_CLASS || type == TokenType.CLASS_DECL || type == TokenType.INTERFACE_DECL || - type == TokenType.ENUM_DECL || type == TokenType.NEW_TYPE; + type == TokenType.ENUM_DECL; } public boolean isResolved() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java index acbace01d..d3c619758 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java @@ -18,7 +18,6 @@ public enum TokenType { MODIFIER(0xFFAA00, 90), // public, private, static, final, etc. // Type declarations and references - NEW_TYPE(0xFF55FF, 80), // type after 'new' keyword INTERFACE_DECL(0x55FFFF, 85), // interface names (aqua) ENUM_DECL(0xFF55FF, 85), // enum names (magenta) CLASS_DECL(0x00AAAA, 85), // class names in declarations @@ -76,7 +75,6 @@ public char toColorCode() { case IMPORT_KEYWORD: case MODIFIER: return '6'; // gold - case NEW_TYPE: case ENUM_DECL: return 'd'; // magenta case INTERFACE_DECL: diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 8a0c6c1e3..16d9e6e1c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -98,7 +98,6 @@ public static TokenHoverInfo fromToken(Token token) { case INTERFACE_DECL: case ENUM_DECL: case TYPE_DECL: - case NEW_TYPE: info.extractClassInfo(token); break; From 58e2b4f591d9ea38c8ae5058f15724f93b5a349e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 15:49:54 +0200 Subject: [PATCH 073/337] disabled hover state click --- .../client/gui/util/script/interpreter/hover/HoverState.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java index 4025e0611..65e9d7c2d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java @@ -151,7 +151,7 @@ public void update(int mouseX, int mouseY, ScriptDocument document, public void update(int mouseX, int mouseY, Token token, int tokenX, int tokenY, int tokenW) { lastMouseX = mouseX; lastMouseY = mouseY; - clickToPinEnabled=true; + clickToPinEnabled=false; // If a token has been pinned by click, ignore mouse movement updates if (pinnedToken != null) { // keep pinned tooltip visible From 323502aa5ee4cd4eed5cc658705c695b1a6d03fc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 16:07:28 +0200 Subject: [PATCH 074/337] Removed NewExpressionType for constructor calls, replaced with MethodCallInfo --- .../script/interpreter/MethodCallInfo.java | 38 +++++++++++++++++-- .../script/interpreter/ScriptDocument.java | 22 ++++------- .../util/script/interpreter/ScriptLine.java | 13 ++++--- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java index cae368d4d..12582cc63 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -99,6 +99,8 @@ public enum ErrorType { private String errorMessage; private int errorArgIndex = -1; // Index of the problematic argument (for WRONG_ARG_TYPE) private List argumentTypeErrors = new ArrayList<>(); + + private boolean isConstructor; public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd, int openParenOffset, int closeParenOffset, @@ -123,6 +125,27 @@ public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd, this.isStaticAccess = isStaticAccess; } + /** + * Factory method to create a MethodCallInfo for a constructor call. + * Constructors are represented as method calls where the type itself is the receiver. + */ + public static MethodCallInfo constructor(TypeInfo typeInfo, MethodInfo constructor, + int typeNameStart, int typeNameEnd, + int openParenOffset, int closeParenOffset, + List arguments) { + return new MethodCallInfo( + typeInfo.getSimpleName(), // Constructor name is the type name + typeNameStart, + typeNameEnd, + openParenOffset, + closeParenOffset, + arguments, + typeInfo, // The type itself is the receiver + constructor, // The constructor MethodInfo + false // Constructors are not static access + ).setConstructor(true); + } + // Getters public String getMethodName() { return methodName; @@ -171,6 +194,15 @@ public TypeInfo getExpectedType() { public void setExpectedType(TypeInfo expectedType) { this.expectedType = expectedType; } + + public boolean isConstructor() { + return isConstructor; + } + + public MethodCallInfo setConstructor(boolean isConstructor) { + this.isConstructor = isConstructor; + return this; + } public ErrorType getErrorType() { return errorType; @@ -276,9 +308,9 @@ public void validate() { setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve method '" + methodName + "'"); return; } - - // Check static/instance access - if (isStaticAccess && !resolvedMethod.isStatic()) { + + // Check static/instance access (skip for constructors) + if (!isConstructor && isStaticAccess && !resolvedMethod.isStatic()) { setError(ErrorType.STATIC_ACCESS_ERROR, "Cannot call instance method '" + methodName + "' on a class type"); return; 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 0ec1b8502..1ca326445 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 @@ -1572,18 +1572,7 @@ else if (c == '>') } } - /** - * Wrapper class to hold both TypeInfo and constructor MethodInfo for "new" expressions. - */ - static class NewExpressionInfo { - final TypeInfo typeInfo; - final MethodInfo constructor; - - NewExpressionInfo(TypeInfo typeInfo, MethodInfo constructor) { - this.typeInfo = typeInfo; - this.constructor = constructor; - } - } + /** * Recursively parse and mark generic type parameters. @@ -3392,9 +3381,12 @@ private void markImportedClassUsages(List marks) { if (info.hasConstructors()) { MethodInfo constructor = info.findConstructor(argCount); if (constructor != null) { - // Attach both type info and constructor - NewExpressionInfo newExpr = new NewExpressionInfo(info, constructor); - marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), newExpr)); + // Create MethodCallInfo for constructor + MethodCallInfo ctorCall = MethodCallInfo.constructor( + info, constructor, start, end, openParen, closeParen, arguments + ); + ctorCall.validate(); + marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), ctorCall)); continue; } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 4fd6d5cdc..ae5f6d0ab 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -152,10 +152,13 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume if (mark.metadata != null) { if (mark.metadata instanceof TypeInfo) { token.setTypeInfo((TypeInfo) mark.metadata); - } else if (mark.metadata instanceof ScriptDocument.NewExpressionInfo) { - ScriptDocument.NewExpressionInfo newExpr = (ScriptDocument.NewExpressionInfo) mark.metadata; - token.setTypeInfo(newExpr.typeInfo); - token.setMethodInfo(newExpr.constructor); + } else if (mark.metadata instanceof MethodCallInfo) { + MethodCallInfo callInfo = (MethodCallInfo) mark.metadata; + if (callInfo.isConstructor()) { + token.setTypeInfo(callInfo.getReceiverType()); + token.setMethodInfo(callInfo.getResolvedMethod()); + } + token.setMethodCallInfo(callInfo); } else if (mark.metadata instanceof FieldInfo.ArgInfo) { FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) mark.metadata; token.setFieldInfo(ctx.fieldInfo); @@ -164,8 +167,6 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume token.setFieldInfo((FieldInfo) mark.metadata); } else if (mark.metadata instanceof MethodInfo) { token.setMethodInfo((MethodInfo) mark.metadata); - } else if (mark.metadata instanceof MethodCallInfo) { - token.setMethodCallInfo((MethodCallInfo) mark.metadata); } else if (mark.metadata instanceof ImportData) { token.setImportData((ImportData) mark.metadata); } else if (mark.metadata instanceof FieldAccessInfo) { From 8b95d61e0f3daec3c6d8deb3872bce4a695721d7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 16:19:15 +0200 Subject: [PATCH 075/337] Added constructor error validation --- .../script/interpreter/MethodCallInfo.java | 13 ++++++++++- .../script/interpreter/ScriptDocument.java | 23 +++++++++---------- .../interpreter/hover/TokenHoverInfo.java | 2 +- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java index 12582cc63..cc53ec2d4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java @@ -305,7 +305,18 @@ public Argument getArg() { */ public void validate() { if (resolvedMethod == null) { - setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve method '" + methodName + "'"); + if (isConstructor) { + // For constructors, check if the type has any constructors at all + if (receiverType != null && receiverType.hasConstructors()) { + setError(ErrorType.WRONG_ARG_COUNT, + "No constructor in '" + methodName + "' matches " + arguments.size() + " argument(s)"); + } else { + setError(ErrorType.UNRESOLVED_METHOD, + "Cannot resolve constructor for '" + methodName + "'"); + } + } else { + setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve method '" + methodName + "'"); + } return; } 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 1ca326445..bd0458c13 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 @@ -3378,18 +3378,17 @@ private void markImportedClassUsages(List marks) { List arguments = parseMethodArguments(openParen + 1, closeParen); int argCount = arguments.size(); - if (info.hasConstructors()) { - MethodInfo constructor = info.findConstructor(argCount); - if (constructor != null) { - // Create MethodCallInfo for constructor - MethodCallInfo ctorCall = MethodCallInfo.constructor( - info, constructor, start, end, openParen, closeParen, arguments - ); - ctorCall.validate(); - marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), ctorCall)); - continue; - } - } + // Try to find matching constructor (may be null if not found) + MethodInfo constructor = info.hasConstructors() ? info.findConstructor(argCount) : null; + + // Create MethodCallInfo for constructor (even if null, so errors are tracked) + MethodCallInfo ctorCall = MethodCallInfo.constructor( + info, constructor, start, end, openParen, closeParen, arguments + ); + ctorCall.validate(); + methodCalls.add(ctorCall); // Add to methodCalls list for error tracking + marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), ctorCall)); + continue; } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 16d9e6e1c..9f342c9d6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -171,7 +171,7 @@ private void extractErrors(Token token) { MethodCallInfo callInfo = token.getMethodCallInfo(); if (callInfo != null) { if (callInfo.hasArgCountError()) { - errors.add("Expected " + getExpectedArgCount(callInfo) + " argument(s) but found " + callInfo.getArguments().size()); + errors.add(callInfo.getErrorMessage()); } if (callInfo.hasArgTypeError()) { for (MethodCallInfo.ArgumentTypeError error : callInfo.getArgumentTypeErrors()) { From c1e4e824abd4be5a8dd1762935eeadbc535e9b58 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 16:25:07 +0200 Subject: [PATCH 076/337] Fixed wrapText not splitting at \n --- .../interpreter/hover/TokenHoverRenderer.java | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index da269737b..4903ddb9a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -281,27 +281,37 @@ private static List wrapText(String text, int maxWidth) { return lines; } - // Fallback: simple word wrapping - String[] words = text.split(" "); - StringBuilder currentLine = new StringBuilder(); + // First split by explicit newlines + String[] paragraphs = text.split("\n", -1); // -1 to preserve trailing empty lines - for (String word : words) { - String testLine = currentLine.length() == 0 ? word : currentLine + " " + word; - int testWidth = ClientProxy.Font.width(testLine); + for (String paragraph : paragraphs) { + if (paragraph.isEmpty()) { + lines.add(""); + continue; + } - if (testWidth > maxWidth && currentLine.length() > 0) { - lines.add(currentLine.toString()); - currentLine = new StringBuilder(word); - } else { - if (currentLine.length() > 0) { - currentLine.append(" "); + // Then apply word wrapping to each paragraph + String[] words = paragraph.split(" "); + StringBuilder currentLine = new StringBuilder(); + + for (String word : words) { + String testLine = currentLine.length() == 0 ? word : currentLine + " " + word; + int testWidth = ClientProxy.Font.width(testLine); + + if (testWidth > maxWidth && currentLine.length() > 0) { + lines.add(currentLine.toString()); + currentLine = new StringBuilder(word); + } else { + if (currentLine.length() > 0) { + currentLine.append(" "); + } + currentLine.append(word); } - currentLine.append(word); } - } - - if (currentLine.length() > 0) { - lines.add(currentLine.toString()); + + if (currentLine.length() > 0) { + lines.add(currentLine.toString()); + } } if (lines.isEmpty()) { From 9aa23ef49d75f2832ad7f8ca7be69a2c8667eb92 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 16:32:36 +0200 Subject: [PATCH 077/337] Added external type constructor validation --- .../util/script/interpreter/MethodInfo.java | 23 ++++++++++++- .../gui/util/script/interpreter/TypeInfo.java | 33 +++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index d1f5ef72f..c8bee1855 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; @@ -85,7 +87,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { * Create a MethodInfo from reflection data. * Used when resolving method calls on known types. */ - public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInfo containingType) { + public static MethodInfo fromReflection(Method method, TypeInfo containingType) { String name = method.getName(); TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); int modifiers = method.getModifiers(); @@ -100,6 +102,25 @@ public static MethodInfo fromReflection(java.lang.reflect.Method method, TypeInf return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, modifiers, null); } + /** + * Create a MethodInfo from a Constructor via reflection. + * Used when resolving constructor calls on external types. + */ + public static MethodInfo fromReflectionConstructor(Constructor constructor, TypeInfo containingType) { + String name = containingType.getSimpleName(); // Constructor name is the type name + TypeInfo returnType = containingType; // Constructor "returns" an instance of the type + int modifiers = constructor.getModifiers(); + + List params = new ArrayList<>(); + Class[] paramTypes = constructor.getParameterTypes(); + for (int i = 0; i < paramTypes.length; i++) { + TypeInfo paramType = TypeInfo.fromClass(paramTypes[i]); + params.add(FieldInfo.reflectionParam("arg" + i, paramType)); + } + + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, true, modifiers, null); + } + // Getters public String getName() { return name; } public TypeInfo getReturnType() { return returnType; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java index 2184f42a6..6216fe9f0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java @@ -184,7 +184,13 @@ public boolean hasMethod(String methodName, int paramCount) { * Override in ScriptTypeInfo for script-defined types. */ public boolean hasConstructors() { - return false; + if (javaClass == null) return false; + try { + return javaClass.getConstructors().length > 0; + } catch (Exception e) { + // Security or linkage error + return false; + } } /** @@ -192,7 +198,18 @@ public boolean hasConstructors() { * Override in ScriptTypeInfo for script-defined types. */ public List getConstructors() { - return new ArrayList<>(); + List result = new ArrayList<>(); + if (javaClass == null) return result; + + try { + java.lang.reflect.Constructor[] constructors = javaClass.getConstructors(); + for (java.lang.reflect.Constructor ctor : constructors) { + result.add(MethodInfo.fromReflectionConstructor(ctor, this)); + } + } catch (Exception e) { + // Security or linkage error + } + return result; } /** @@ -200,6 +217,18 @@ public List getConstructors() { * Override in ScriptTypeInfo for script-defined types. */ public MethodInfo findConstructor(int argCount) { + if (javaClass == null) return null; + + try { + java.lang.reflect.Constructor[] constructors = javaClass.getConstructors(); + for (java.lang.reflect.Constructor ctor : constructors) { + if (ctor.getParameterCount() == argCount) { + return MethodInfo.fromReflectionConstructor(ctor, this); + } + } + } catch (Exception e) { + // Security or linkage error + } return null; } From b7333bcf6a570177246e20aa2c183539333344e5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 18:53:29 +0200 Subject: [PATCH 078/337] Captured full/type/name offsets of method declarations --- .../util/script/interpreter/MethodInfo.java | 54 ++++++++---- .../script/interpreter/ScriptDocument.java | 88 ++++++++++++++++++- 2 files changed, 123 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index c8bee1855..9e9ed9039 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -5,11 +5,20 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Metadata for a method declaration or method call. * Tracks method name, parameters, return type, and containing class. + * + *

This class delegates to helper classes for specific functionality:

+ *
    + *
  • {@link ControlFlowAnalyzer} - Control flow analysis for missing return detection
  • + *
  • {@link CodeParser} - Code parsing utilities (brace matching, comment removal)
  • + *
  • {@link TypeChecker} - Type compatibility checking
  • + *
*/ public final class MethodInfo { @@ -17,7 +26,9 @@ public final class MethodInfo { private final TypeInfo returnType; private final TypeInfo containingType; // The class/interface that owns this method private final List parameters; - private final int declarationOffset; // -1 for external methods (calls to library methods) + private final int fullDeclarationOffset; // Start of full declaration (including modifiers), -1 for external + private final int typeOffset; // Start of return type + private final int nameOffset; // Start of method name private final int bodyStart; // Start of method body (after {) private final int bodyEnd; // End of method body (before }) private final boolean resolved; @@ -26,14 +37,16 @@ public final class MethodInfo { private final String documentation; // Javadoc/comment documentation for this method private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, - List parameters, int declarationOffset, + List parameters, int fullDeclarationOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration, int modifiers, String documentation) { this.name = name; this.returnType = returnType; this.containingType = containingType; this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>(); - this.declarationOffset = declarationOffset; + this.fullDeclarationOffset = fullDeclarationOffset; + this.typeOffset = typeOffset; + this.nameOffset = nameOffset; this.bodyStart = bodyStart; this.bodyEnd = bodyEnd; this.resolved = resolved; @@ -44,25 +57,29 @@ private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, // Factory methods public static MethodInfo declaration(String name, TypeInfo returnType, List params, - int declOffset, int bodyStart, int bodyEnd) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, 0, null); + int fullDeclOffset, int typeOffset, int nameOffset, + int bodyStart, int bodyEnd) { + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, 0, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, - int declOffset, int bodyStart, int bodyEnd, boolean isStatic) { + int fullDeclOffset, int typeOffset, int nameOffset, + int bodyStart, int bodyEnd, boolean isStatic) { int modifiers = isStatic ? Modifier.STATIC : 0; - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, modifiers, null); + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, - int declOffset, int bodyStart, int bodyEnd, boolean isStatic, String documentation) { + int fullDeclOffset, int typeOffset, int nameOffset, + int bodyStart, int bodyEnd, boolean isStatic, String documentation) { int modifiers = isStatic ? Modifier.STATIC : 0; - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, - int declOffset, int bodyStart, int bodyEnd, int modifiers, String documentation) { - return new MethodInfo(name, returnType, null, params, declOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); + int fullDeclOffset, int typeOffset, int nameOffset, + int bodyStart, int bodyEnd, int modifiers, String documentation) { + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); } public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { @@ -72,7 +89,7 @@ public static MethodInfo call(String name, TypeInfo containingType, int paramCou for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, containingType, params, -1, -1, -1, resolved, false, 0, null); + return new MethodInfo(name, null, containingType, params, -1, -1, -1, -1, -1, resolved, false, 0, null); } public static MethodInfo unresolvedCall(String name, int paramCount) { @@ -80,7 +97,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, null, params, -1, -1, -1, false, false, 0, null); + return new MethodInfo(name, null, null, params, -1, -1, -1, -1, -1, false, false, 0, null); } /** @@ -99,7 +116,7 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, false, modifiers, null); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, null); } /** @@ -118,7 +135,7 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, true, true, modifiers, null); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, true, modifiers, null); } // Getters @@ -127,7 +144,12 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T public TypeInfo getContainingType() { return containingType; } public List getParameters() { return Collections.unmodifiableList(parameters); } public int getParameterCount() { return parameters.size(); } - public int getDeclarationOffset() { return declarationOffset; } + /** @deprecated Use getTypeOffset() or getNameOffset() instead */ + @Deprecated + public int getDeclarationOffset() { return typeOffset; } + public int getFullDeclarationOffset() { return fullDeclarationOffset; } + public int getTypeOffset() { return typeOffset; } + public int getNameOffset() { return nameOffset; } public int getBodyStart() { return bodyStart; } public int getBodyEnd() { return bodyEnd; } public boolean isResolved() { return resolved; } 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 bd0458c13..73874410c 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 @@ -455,8 +455,23 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { List params = parseParametersWithPositions(paramList, bodyStart + 1 + mm.start(3)); + // Calculate the three offsets + int typeOffset = bodyStart + 1 + mm.start(1); // Start of return type + int nameOffset = bodyStart + 1 + mm.start(2); // Start of method name + int fullDeclOffset = findFullDeclarationStart(mm.start(1), bodyText); + if (fullDeclOffset >= 0) { + fullDeclOffset += bodyStart + 1; + } else { + fullDeclOffset = typeOffset; + } + MethodInfo methodInfo = MethodInfo.declaration( - methodName, returnType, params, absPos, methodBodyStart, methodBodyEnd, modifiers, documentation); + methodName, returnType, params, fullDeclOffset, typeOffset, nameOffset, methodBodyStart, methodBodyEnd, modifiers, documentation); + + // Validate the method (including return type checking) + String methodBodyText = text.substring(methodBodyStart + 1, methodBodyEnd); + methodInfo.validate(methodBodyText, (expr, pos) -> resolveExpressionType(expr, pos)); + scriptType.addMethod(methodInfo); } @@ -485,12 +500,25 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { // Parse parameters with their actual positions List params = parseParametersWithPositions(paramList, bodyStart + 1 + cm.start(1)); + // Calculate the three offsets for constructor + // For constructors, type offset and name offset are the same (start of constructor name) + int nameOffset = bodyStart + 1 + cm.start(); + int typeOffset = nameOffset; // No separate type for constructors + int fullDeclOffset = findFullDeclarationStart(cm.start(), bodyText); + if (fullDeclOffset >= 0) { + fullDeclOffset += bodyStart + 1; + } else { + fullDeclOffset = nameOffset; + } + // Constructors don't have a return type, but we'll use the containing type as a marker MethodInfo constructorInfo = MethodInfo.declaration( typeName, scriptType, // Return type is the type itself params, - absPos, + fullDeclOffset, + typeOffset, + nameOffset, constructorBodyStart, constructorBodyEnd, modifiers, @@ -547,6 +575,11 @@ private void parseMethodDeclarations() { // Extract modifiers by scanning backwards from match start int modifiers = extractModifiersBackwards(m.start() - 1, text); + + // Calculate offset positions + int typeOffset = m.start(1); // Start of return type + int nameOffset = m.start(2); // Start of method name + int fullDeclOffset = findFullDeclarationStart(m.start(1), text); // Start including modifiers // Parse parameters with their actual positions List params = parseParametersWithPositions(paramList, m.start(3)); @@ -555,12 +588,19 @@ private void parseMethodDeclarations() { methodName, resolveType(returnType), params, - m.start(), + fullDeclOffset, + typeOffset, + nameOffset, bodyStart, bodyEnd, modifiers, documentation ); + + // Validate the method (return statements, parameters) with type resolution + String methodBodyText = bodyEnd > bodyStart + 1 ? text.substring(bodyStart + 1, bodyEnd) : ""; + methodInfo.validate(methodBodyText, (expr, pos) -> resolveExpressionType(expr, pos)); + methods.add(methodInfo); } @@ -584,6 +624,48 @@ private void parseMethodDeclarations() { } } + /** + * Find the start of a full method declaration, including any modifiers before the type. + */ + private int findFullDeclarationStart(int typeStart, String sourceText) { + int pos = typeStart - 1; + + // Skip whitespace before type + while (pos >= 0 && Character.isWhitespace(sourceText.charAt(pos))) { + pos--; + } + + // Check for modifiers + int earliestPos = typeStart; + while (pos >= 0) { + // Skip whitespace + while (pos >= 0 && Character.isWhitespace(sourceText.charAt(pos))) { + pos--; + } + if (pos < 0) break; + + // Read word backwards + int wordEnd = pos + 1; + while (pos >= 0 && Character.isJavaIdentifierPart(sourceText.charAt(pos))) { + pos--; + } + int wordStart = pos + 1; + + if (wordStart < wordEnd) { + String word = sourceText.substring(wordStart, wordEnd); + if (TypeResolver.isModifier(word)) { + earliestPos = wordStart; + } else { + break; + } + } else { + break; + } + } + + return earliestPos; + } + private List parseParametersWithPositions(String paramList, int paramListStart) { List params = new ArrayList<>(); if (paramList == null || paramList.trim().isEmpty()) { From 3e21757d12d3d417d262330c57b6e4aa1aeb8f02 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 18:54:10 +0200 Subject: [PATCH 079/337] Method declaration error validation + helper classes needed for that --- .../util/script/interpreter/CodeParser.java | 323 ++++++++++++++++++ .../interpreter/ControlFlowAnalyzer.java | 215 ++++++++++++ .../util/script/interpreter/MethodInfo.java | 261 ++++++++++++++ .../util/script/interpreter/ScriptLine.java | 120 +++++++ .../util/script/interpreter/TypeChecker.java | 161 +++++++++ .../interpreter/hover/TokenHoverInfo.java | 74 ++++ 6 files changed, 1154 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/CodeParser.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ControlFlowAnalyzer.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeChecker.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/CodeParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/CodeParser.java new file mode 100644 index 000000000..435c8f42e --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/CodeParser.java @@ -0,0 +1,323 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Utility class for parsing and analyzing code text. + * Provides methods for finding matching braces/parentheses, removing comments/strings, + * and locating keywords in code. + */ +public final class CodeParser { + + private CodeParser() {} // Utility class + + // ==================== Matching Delimiters ==================== + + /** + * Find the closing brace that matches the opening brace at the given position. + * @param text The text to search + * @param openBrace Position of the opening '{' + * @return Position of matching '}' or -1 if not found + */ + public static int findMatchingBrace(String text, int openBrace) { + if (openBrace < 0 || openBrace >= text.length() || text.charAt(openBrace) != '{') { + return -1; + } + int depth = 1; + for (int i = openBrace + 1; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '{') depth++; + else if (c == '}') { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + /** + * Find the closing parenthesis that matches the opening parenthesis at the given position. + * @param text The text to search + * @param openParen Position of the opening '(' + * @return Position of matching ')' or -1 if not found + */ + public static int findMatchingParen(String text, int openParen) { + if (openParen < 0 || openParen >= text.length() || text.charAt(openParen) != '(') { + return -1; + } + int depth = 1; + for (int i = openParen + 1; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '(') depth++; + else if (c == ')') { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + // ==================== Comment/String Removal ==================== + + /** + * Remove string literals and comments from code for structural analysis. + * This is used when analyzing control flow to avoid false positives from + * keywords inside strings or comments. + * @param code The source code + * @return Code with strings and comments removed + */ + public static String removeStringsAndComments(String code) { + StringBuilder result = new StringBuilder(); + boolean inString = false; + boolean inChar = false; + boolean inLineComment = false; + boolean inBlockComment = false; + + for (int i = 0; i < code.length(); i++) { + char c = code.charAt(i); + char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0; + + if (inLineComment) { + if (c == '\n') { + inLineComment = false; + result.append(c); + } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { + inBlockComment = false; + i++; // skip '/' + } + continue; + } + + if (inString || inChar) { + if (c == '\\' && i + 1 < code.length()) { + i++; // skip escaped character + continue; + } + if ((inString && c == '"') || (inChar && c == '\'')) { + inString = false; + inChar = false; + } + continue; + } + + // Check for start of comments or strings + if (c == '/' && next == '/') { + inLineComment = true; + i++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + i++; + continue; + } + if (c == '"') { + inString = true; + continue; + } + if (c == '\'') { + inChar = true; + continue; + } + + result.append(c); + } + + return result.toString(); + } + + /** + * Remove only comments from code, keeping strings intact and preserving positions. + * Comments are replaced with spaces to maintain character positions. + * @param code The source code + * @return Code with comments replaced by spaces + */ + public static String removeComments(String code) { + StringBuilder result = new StringBuilder(); + boolean inString = false; + boolean inChar = false; + boolean inLineComment = false; + boolean inBlockComment = false; + + for (int i = 0; i < code.length(); i++) { + char c = code.charAt(i); + char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0; + + if (inLineComment) { + if (c == '\n') { + inLineComment = false; + result.append(c); + } else { + result.append(' '); // Preserve position with spaces + } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { + inBlockComment = false; + result.append(" "); // Preserve position + i++; + } else { + result.append(c == '\n' ? '\n' : ' '); // Preserve newlines + } + continue; + } + + if (inString) { + result.append(c); + if (c == '\\' && i + 1 < code.length()) { + result.append(code.charAt(++i)); + continue; + } + if (c == '"') inString = false; + continue; + } + + if (inChar) { + result.append(c); + if (c == '\\' && i + 1 < code.length()) { + result.append(code.charAt(++i)); + continue; + } + if (c == '\'') inChar = false; + continue; + } + + // Check for start of comments + if (c == '/' && next == '/') { + inLineComment = true; + result.append(" "); // Preserve position + i++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + result.append(" "); // Preserve position + i++; + continue; + } + if (c == '"') { inString = true; } + if (c == '\'') { inChar = true; } + + result.append(c); + } + + return result.toString(); + } + + // ==================== Keyword Detection ==================== + + /** + * Check if a keyword exists at the given position in text. + * Verifies the keyword is not part of a larger identifier. + * @param text The text to check + * @param pos Position to check + * @param keyword The keyword to look for + * @return true if the keyword is at this position as a standalone word + */ + public static boolean isKeywordAt(String text, int pos, String keyword) { + if (pos + keyword.length() > text.length()) return false; + if (!text.substring(pos).startsWith(keyword)) return false; + + boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1)); + boolean validAfter = pos + keyword.length() >= text.length() + || !Character.isLetterOrDigit(text.charAt(pos + keyword.length())); + + return validBefore && validAfter; + } + + /** + * Find the next "return" keyword that is not inside a string literal. + * @param text The text to search + * @param start Position to start searching from + * @return Position of "return" or -1 if not found + */ + public static int findReturnKeyword(String text, int start) { + boolean inString = false; + boolean inChar = false; + + for (int i = start; i < text.length() - 5; i++) { + char c = text.charAt(i); + + if (inString) { + if (c == '\\' && i + 1 < text.length()) { i++; continue; } + if (c == '"') inString = false; + continue; + } + if (inChar) { + if (c == '\\' && i + 1 < text.length()) { i++; continue; } + if (c == '\'') inChar = false; + continue; + } + + if (c == '"') { inString = true; continue; } + if (c == '\'') { inChar = true; continue; } + + // Check for "return" keyword + if (isKeywordAt(text, i, "return")) { + return i; + } + } + return -1; + } + + /** + * Find the semicolon that ends a return statement, handling nested structures. + * @param text The text to search + * @param start Position to start searching (after "return" keyword) + * @return Position of ';' or -1 if not found + */ + public static int findReturnSemicolon(String text, int start) { + int parenDepth = 0; + int braceDepth = 0; + boolean inString = false; + boolean inChar = false; + + for (int i = start; i < text.length(); i++) { + char c = text.charAt(i); + + if (inString) { + if (c == '\\' && i + 1 < text.length()) { i++; continue; } + if (c == '"') inString = false; + continue; + } + if (inChar) { + if (c == '\\' && i + 1 < text.length()) { i++; continue; } + if (c == '\'') inChar = false; + continue; + } + + if (c == '"') { inString = true; continue; } + if (c == '\'') { inChar = true; continue; } + if (c == '(') { parenDepth++; continue; } + if (c == ')') { parenDepth--; continue; } + if (c == '{') { braceDepth++; continue; } + if (c == '}') { braceDepth--; continue; } + + if (c == ';' && parenDepth == 0 && braceDepth == 0) { + return i; + } + } + return -1; + } + + // ==================== Whitespace Utilities ==================== + + /** + * Skip whitespace characters starting from the given position. + * @param text The text + * @param pos Starting position + * @return Position of first non-whitespace character + */ + public static int skipWhitespace(String text, int pos) { + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) { + pos++; + } + return pos; + } +} + diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ControlFlowAnalyzer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ControlFlowAnalyzer.java new file mode 100644 index 000000000..6416805ba --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ControlFlowAnalyzer.java @@ -0,0 +1,215 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Analyzes control flow in method bodies to determine if all paths return a value. + * Used for detecting "missing return statement" errors. + */ +public final class ControlFlowAnalyzer { + + private ControlFlowAnalyzer() {} // Utility class + + /** + * Check if the method body has a guaranteed return on all code paths. + */ + public static boolean hasGuaranteedReturn(String body) { + String cleanBody = CodeParser.removeStringsAndComments(body); + + return hasTopLevelReturn(cleanBody) + || hasCompleteIfElseReturn(cleanBody) + || hasTryFinallyReturn(cleanBody) + || hasCompleteSwitchReturn(cleanBody); + } + + /** + * Check for a bare return statement at the method's top level (brace depth 0). + */ + private static boolean hasTopLevelReturn(String body) { + int braceDepth = 0; + int parenDepth = 0; + + for (int i = 0; i < body.length(); i++) { + char c = body.charAt(i); + + if (c == '{') braceDepth++; + else if (c == '}') braceDepth--; + else if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + + if (braceDepth == 0 && parenDepth == 0 && isKeywordAt(body, i, "return")) { + return true; + } + } + return false; + } + + /** + * Check for complete if-else chains where all branches return. + */ + private static boolean hasCompleteIfElseReturn(String body) { + int braceDepth = 0; + + for (int i = 0; i < body.length(); i++) { + char c = body.charAt(i); + if (c == '{') { braceDepth++; continue; } + if (c == '}') { braceDepth--; continue; } + + if (braceDepth == 0 && isKeywordAt(body, i, "if")) { + if (checkIfElseChainReturns(body, i)) { + return true; + } + } + } + return false; + } + + /** + * Check if an if-else chain starting at the given position guarantees a return. + * Handles: if(true) { return; } - always returns (else unreachable) + * if(false) { } else { return; } - else always executes + */ + private static boolean checkIfElseChainReturns(String body, int ifStart) { + int pos = ifStart; + + while (pos < body.length()) { + if (!body.substring(pos).startsWith("if")) return false; + pos += 2; + + pos = skipWhitespace(body, pos); + if (pos >= body.length() || body.charAt(pos) != '(') return false; + + int condStart = pos; + int condEnd = CodeParser.findMatchingParen(body, pos); + if (condEnd < 0) return false; + + // Check for literal true/false conditions + String condition = body.substring(condStart + 1, condEnd).trim(); + boolean isAlwaysTrue = condition.equals("true"); + boolean isAlwaysFalse = condition.equals("false"); + + pos = skipWhitespace(body, condEnd + 1); + if (pos >= body.length() || body.charAt(pos) != '{') return false; + + int blockEnd = CodeParser.findMatchingBrace(body, pos); + if (blockEnd < 0) return false; + + String ifBlock = body.substring(pos + 1, blockEnd); + boolean ifBlockReturns = hasGuaranteedReturn(ifBlock); + + // if(true) with return means we always return (else is unreachable) + if (isAlwaysTrue && ifBlockReturns) { + return true; + } + + pos = skipWhitespace(body, blockEnd + 1); + + // Must have else clause for guaranteed return + if (!body.substring(pos).startsWith("else")) { + return false; + } + pos += 4; + pos = skipWhitespace(body, pos); + + // else if - continue the chain + if (isKeywordAt(body, pos, "if")) { + continue; + } + + // Plain else block - final branch + if (pos >= body.length() || body.charAt(pos) != '{') return false; + int elseBlockEnd = CodeParser.findMatchingBrace(body, pos); + if (elseBlockEnd < 0) return false; + + String elseBlock = body.substring(pos + 1, elseBlockEnd); + boolean elseBlockReturns = hasGuaranteedReturn(elseBlock); + + // For if-else to guarantee return: both branches must return + // Special case: if(false) means if-block never executes, so only else matters + if (isAlwaysFalse) { + return elseBlockReturns; + } + + // Normal case: both if and else must return + return ifBlockReturns && elseBlockReturns; + } + return false; + } + + /** + * Check for try-finally where finally block returns. + */ + private static boolean hasTryFinallyReturn(String body) { + for (int i = 0; i < body.length(); i++) { + if (!isKeywordAt(body, i, "try")) continue; + + int tryBlockStart = body.indexOf('{', i); + if (tryBlockStart < 0) continue; + + int tryBlockEnd = CodeParser.findMatchingBrace(body, tryBlockStart); + if (tryBlockEnd < 0) continue; + + // Skip catch blocks + int searchPos = skipCatchBlocks(body, tryBlockEnd + 1); + searchPos = skipWhitespace(body, searchPos); + + // Check for finally + if (body.substring(searchPos).startsWith("finally")) { + int finallyBlockStart = body.indexOf('{', searchPos); + if (finallyBlockStart >= 0) { + int finallyBlockEnd = CodeParser.findMatchingBrace(body, finallyBlockStart); + if (finallyBlockEnd >= 0) { + String finallyBlock = body.substring(finallyBlockStart + 1, finallyBlockEnd); + if (hasGuaranteedReturn(finallyBlock)) { + return true; + } + } + } + } + } + return false; + } + + private static int skipCatchBlocks(String body, int start) { + int pos = start; + while (true) { + pos = skipWhitespace(body, pos); + if (pos >= body.length() || !body.substring(pos).startsWith("catch")) break; + + int catchBlockStart = body.indexOf('{', pos); + if (catchBlockStart < 0) break; + + int catchBlockEnd = CodeParser.findMatchingBrace(body, catchBlockStart); + if (catchBlockEnd < 0) break; + + pos = catchBlockEnd + 1; + } + return pos; + } + + /** + * Check for switch with default where all cases return. + * Returns false for now - full implementation is complex. + */ + private static boolean hasCompleteSwitchReturn(String body) { + return false; + } + + // ==================== Utility Methods ==================== + + private static boolean isKeywordAt(String text, int pos, String keyword) { + if (pos + keyword.length() > text.length()) return false; + if (!text.substring(pos).startsWith(keyword)) return false; + + boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1)); + boolean validAfter = pos + keyword.length() >= text.length() + || !Character.isLetterOrDigit(text.charAt(pos + keyword.length())); + + return validBefore && validAfter; + } + + private static int skipWhitespace(String text, int pos) { + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) { + pos++; + } + return pos; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 9e9ed9039..5614c2fec 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -22,6 +22,17 @@ */ public final class MethodInfo { + /** + * Validation error types for method declarations. + */ + public enum ErrorType { + NONE, + MISSING_RETURN, // Non-void method missing return statement + RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type + DUPLICATE_PARAMETER, // Two parameters have the same name + PARAMETER_UNDEFINED // Parameter type cannot be resolved + } + private final String name; private final TypeInfo returnType; private final TypeInfo containingType; // The class/interface that owns this method @@ -36,6 +47,12 @@ public final class MethodInfo { private final int modifiers; // Java Modifier flags (e.g., Modifier.PUBLIC | Modifier.STATIC) private final String documentation; // Javadoc/comment documentation for this method + // Error tracking for method declarations + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + private List parameterErrors = new ArrayList<>(); + private List returnStatementErrors = new ArrayList<>(); + private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, List parameters, int fullDeclarationOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration, @@ -237,4 +254,248 @@ public boolean equals(Object o) { public int hashCode() { return name.hashCode() * 31 + parameters.size(); } + + // ==================== ERROR HANDLING ==================== + + /** + * Represents an error with a specific parameter. + */ + public static class ParameterError { + private final FieldInfo parameter; + private final int paramIndex; + private final ErrorType errorType; + private final String message; + + public ParameterError(FieldInfo parameter, int paramIndex, ErrorType errorType, String message) { + this.parameter = parameter; + this.paramIndex = paramIndex; + this.errorType = errorType; + this.message = message; + } + + public FieldInfo getParameter() { return parameter; } + public int getParamIndex() { return paramIndex; } + public ErrorType getErrorType() { return errorType; } + public String getMessage() { return message; } + } + + /** + * Represents an error with a return statement (type mismatch). + */ + public static class ReturnStatementError { + private final int startOffset; // Start of "return" keyword (absolute position) + private final int endOffset; // End of return statement (after semicolon) + private final String message; + private final TypeInfo expectedType; + private final TypeInfo actualType; + + public ReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) { + this.startOffset = startOffset; + this.endOffset = endOffset; + this.message = message; + this.expectedType = expectedType; + this.actualType = actualType; + } + + public int getStartOffset() { return startOffset; } + public int getEndOffset() { return endOffset; } + public String getMessage() { return message; } + public TypeInfo getExpectedType() { return expectedType; } + public TypeInfo getActualType() { return actualType; } + } + + /** + * Check if this method declaration has any errors. + */ + public boolean hasError() { + return errorType != ErrorType.NONE || !parameterErrors.isEmpty() || !returnStatementErrors.isEmpty(); + } + + /** + * Check if this has a missing return error. + */ + public boolean hasMissingReturnError() { + return errorType == ErrorType.MISSING_RETURN; + } + + /** + * Check if this has parameter errors. + */ + public boolean hasParameterErrors() { + return !parameterErrors.isEmpty(); + } + + /** + * Check if this has return statement type errors. + */ + public boolean hasReturnStatementErrors() { + return !returnStatementErrors.isEmpty(); + } + + public ErrorType getErrorType() { return errorType; } + public String getErrorMessage() { return errorMessage; } + public List getParameterErrors() { return Collections.unmodifiableList(parameterErrors); } + public List getReturnStatementErrors() { return Collections.unmodifiableList(returnStatementErrors); } + + /** + * Set the main error for this method. + */ + public void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + /** + * Add a parameter-specific error. + */ + public void addParameterError(FieldInfo param, int index, ErrorType type, String message) { + parameterErrors.add(new ParameterError(param, index, type, message)); + } + + /** + * Add a return statement error. + */ + public void addReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) { + returnStatementErrors.add(new ReturnStatementError(startOffset, endOffset, message, expectedType, actualType)); + } + + /** + * Functional interface for type resolution callback. + */ + @FunctionalInterface + public interface TypeResolver { + TypeInfo resolveExpression(String expression, int position); + } + + /** + * Validate this method declaration. + * Checks for parameter errors and missing return statements. + * + * @param methodBodyText The text content of the method body (between { and }) + */ + public void validate(String methodBodyText) { + validate(methodBodyText, null); + } + + /** + * Validate this method declaration with type resolution for return statements. + * Checks for parameter errors, missing return statements, and return type mismatches. + * + * @param methodBodyText The text content of the method body (between { and }) + * @param typeResolver Optional callback to resolve expression types (for return type checking) + */ + public void validate(String methodBodyText, TypeResolver typeResolver) { + if (!isDeclaration) return; + + // Validate parameters + validateParameters(); + + // Validate return statement types FIRST if we have a type resolver + // This ensures type errors are shown even if there's also a missing return + if (typeResolver != null) { + validateReturnTypes(methodBodyText, typeResolver); + } + + // Validate return statement for non-void methods + // This may set the main error, but return type errors are already recorded + validateReturnStatement(methodBodyText); + } + + /** + * Check for duplicate and unresolved parameters. + */ + private void validateParameters() { + Set seenNames = new HashSet<>(); + + for (int i = 0; i < parameters.size(); i++) { + FieldInfo param = parameters.get(i); + String paramName = param.getName(); + + // Check for duplicate parameter names + if (seenNames.contains(paramName)) { + addParameterError(param, i, ErrorType.DUPLICATE_PARAMETER, + "Duplicate parameter name '" + paramName + "'"); + } else { + seenNames.add(paramName); + } + + // Check for unresolved parameter types + TypeInfo paramType = param.getDeclaredType(); + if (paramType == null || !paramType.isResolved()) { + String typeName = paramType != null ? paramType.getSimpleName() : "unknown"; + addParameterError(param, i, ErrorType.PARAMETER_UNDEFINED, + "Cannot resolve parameter type '" + typeName + "'"); + } + } + } + + /** + * Validate that a non-void method has a guaranteed return statement. + * Delegates to {@link ControlFlowAnalyzer} for control flow analysis. + */ + private void validateReturnStatement(String bodyText) { + if (bodyText == null || bodyText.isEmpty()) return; + + // void methods don't need return statements + if (TypeChecker.isVoidType(returnType)) return; + + // Abstract/native methods don't have bodies + if (isAbstract() || isNative()) return; + + // Check if the method has a guaranteed return using the control flow analyzer + if (!ControlFlowAnalyzer.hasGuaranteedReturn(bodyText)) { + setError(ErrorType.MISSING_RETURN, "Missing return statement"); + } + } + + /** + * Validate that all return statements have compatible types. + * Delegates to {@link CodeParser} for parsing and {@link TypeChecker} for type checks. + */ + private void validateReturnTypes(String bodyText, TypeResolver typeResolver) { + if (bodyText == null || bodyText.isEmpty()) return; + if (TypeChecker.isVoidType(returnType)) return; + + String expectedTypeName = returnType.getSimpleName(); + + // Remove comments but keep strings (we need accurate positions) + String cleanBody = CodeParser.removeComments(bodyText); + + // Find all return statements + int pos = 0; + while (pos < cleanBody.length()) { + // Look for "return" keyword + int returnPos = CodeParser.findReturnKeyword(cleanBody, pos); + if (returnPos < 0) break; + + // Find the semicolon ending this return statement + int semiPos = CodeParser.findReturnSemicolon(cleanBody, returnPos + 6); + if (semiPos < 0) { + pos = returnPos + 6; + continue; + } + + // Extract the return expression + String returnExpr = cleanBody.substring(returnPos + 6, semiPos).trim(); + + // Skip empty returns (handled by missing return check) + if (!returnExpr.isEmpty()) { + // Calculate absolute position (bodyStart + 1 is after the opening brace) + int absoluteReturnStart = bodyStart + 1 + returnPos; + int absoluteSemiEnd = bodyStart + 1 + semiPos + 1; // +1 to include semicolon + + // Resolve the expression type + TypeInfo actualType = typeResolver.resolveExpression(returnExpr, absoluteReturnStart); + + // Check type compatibility + if (!TypeChecker.isTypeCompatible(returnType, actualType)) { + String actualTypeName = actualType != null ? actualType.getSimpleName() : "null"; + String message = "Incompatible types.\nRequired: " + expectedTypeName + "\nFound: " + actualTypeName; + addReturnStatementError(absoluteReturnStart, absoluteSemiEnd, message, returnType, actualType); + } + } + + pos = semiPos + 1; + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index ae5f6d0ab..455465c2c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -450,6 +450,126 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { drawCurlyUnderline(lineStartX + beforeWidth, baselineY, underlineWidth, 0xFF5555); } } + + // Check all method declarations for errors (missing return, parameter errors, return type errors) + for (MethodInfo method : doc.getMethods()) { + if (!method.isDeclaration() || !method.hasError()) + continue; + + // Handle return statement type errors (underline the entire return statement) + if (method.hasReturnStatementErrors()) { + for (MethodInfo.ReturnStatementError returnError : method.getReturnStatementErrors()) { + int returnStart = returnError.getStartOffset(); + int returnEnd = returnError.getEndOffset(); + + //error 247, 258 + //line 235, 259 + // Skip if return statement doesn't intersect this line + if (returnEnd < lineStart || returnStart > lineEnd) + continue; + + // Clip to line boundaries + int clipStart = Math.max(returnStart, lineStart); + int clipEnd = Math.min(returnEnd, lineEnd); + + if (clipStart >= clipEnd) + continue; + + // Convert to line-local coordinates + int lineLocalStart = clipStart - lineStart; + int lineLocalEnd = clipEnd - lineStart; + + // Bounds check + if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) + continue; + + // Compute pixel position + String beforeReturn = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeReturn); + + int returnWidth; + if (lineLocalEnd > lineText.length()) { + returnWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); + } else { + String returnTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); + returnWidth = ClientProxy.Font.width(returnTextOnLine); + } + + if (returnWidth > 0) { + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, returnWidth, 0xFF5555); + } + } + } + + // Handle missing return error (underline the method name) + if (method.hasMissingReturnError()) { + int methodNameStart = method.getNameOffset(); + int methodNameEnd = methodNameStart + method.getName().length(); + + // Skip if method name doesn't intersect this line + if (methodNameEnd < lineStart || methodNameStart > lineEnd) + continue; + + // Check if method name is on this line + if (methodNameStart >= lineStart && methodNameStart < lineEnd) { + int lineLocalStart = methodNameStart - lineStart; + if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { + String beforeMethod = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeMethod); + int methodWidth = ClientProxy.Font.width(method.getName()); + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); + } + } + } + + // Handle parameter errors (underline specific parameters) + if (method.hasParameterErrors()) { + for (MethodInfo.ParameterError paramError : method.getParameterErrors()) { + FieldInfo param = paramError.getParameter(); + if (param == null || param.getDeclarationOffset() < 0) + continue; + + int paramStart = param.getDeclarationOffset(); + int paramEnd = paramStart + param.getName().length(); + + // Skip if parameter doesn't intersect this line + if (paramEnd < lineStart || paramStart > lineEnd) + continue; + + // Clip to line boundaries + int clipStart = Math.max(paramStart, lineStart); + int clipEnd = Math.min(paramEnd, lineEnd); + + if (clipStart >= clipEnd) + continue; + + // Convert to line-local coordinates + int lineLocalStart = clipStart - lineStart; + int lineLocalEnd = clipEnd - lineStart; + + // Bounds check + if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) + continue; + + // Compute pixel position + String beforeParam = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeParam); + + int paramWidth; + if (lineLocalEnd > lineText.length()) { + paramWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); + } else { + String paramTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); + paramWidth = ClientProxy.Font.width(paramTextOnLine); + } + + if (paramWidth > 0) { + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, paramWidth, 0xFF5555); + } + } + } + + } } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeChecker.java new file mode 100644 index 000000000..8e63507ae --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeChecker.java @@ -0,0 +1,161 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +/** + * Utility class for checking type compatibility between types. + * Handles primitive widening conversions, boxing/unboxing, and inheritance. + */ +public final class TypeChecker { + + private TypeChecker() {} // Utility class + + /** + * Check if the actual type is compatible with (assignable to) the expected type. + * @param expected The expected/target type + * @param actual The actual/source type + * @return true if actual can be assigned to expected + */ + public static boolean isTypeCompatible(TypeInfo expected, TypeInfo actual) { + if (expected == null) return true; // void can accept anything (shouldn't happen) + if (actual == null) return true; // null is compatible with any reference type + + String expectedName = expected.getSimpleName(); + String actualName = actual.getSimpleName(); + + if (expectedName == null || actualName == null) return true; + + // Exact match + if (expectedName.equals(actualName)) return true; + + // Primitive widening conversions + if (isNumericType(expectedName) && isNumericType(actualName)) { + return canWiden(actualName, expectedName); + } + + // Object type compatibility (check inheritance) + if (expected.getJavaClass() != null && actual.getJavaClass() != null) { + return expected.getJavaClass().isAssignableFrom(actual.getJavaClass()); + } + + // Allow boxed/unboxed conversions + if (isPrimitiveOrWrapper(expectedName) && isPrimitiveOrWrapper(actualName)) { + return getUnboxedName(expectedName).equals(getUnboxedName(actualName)); + } + + return false; + } + + /** + * Check if the type is a numeric type (including wrappers). + */ + public static boolean isNumericType(String typeName) { + switch (typeName) { + case "byte": case "Byte": + case "short": case "Short": + case "int": case "Integer": + case "long": case "Long": + case "float": case "Float": + case "double": case "Double": + case "char": case "Character": + return true; + default: + return false; + } + } + + /** + * Check if a primitive type can be widened to another type. + * Follows Java's widening primitive conversion rules. + */ + public static boolean canWiden(String from, String to) { + int fromRank = getNumericRank(from); + int toRank = getNumericRank(to); + return fromRank <= toRank; + } + + /** + * Get the numeric rank for primitive widening conversion. + * Higher rank can accept lower ranks. + */ + public static int getNumericRank(String typeName) { + switch (getUnboxedName(typeName)) { + case "byte": return 1; + case "short": case "char": return 2; + case "int": return 3; + case "long": return 4; + case "float": return 5; + case "double": return 6; + default: return 0; + } + } + + /** + * Check if the type name is a primitive or its wrapper type. + */ + public static boolean isPrimitiveOrWrapper(String typeName) { + switch (typeName) { + case "byte": case "Byte": + case "short": case "Short": + case "int": case "Integer": + case "long": case "Long": + case "float": case "Float": + case "double": case "Double": + case "char": case "Character": + case "boolean": case "Boolean": + return true; + default: + return false; + } + } + + /** + * Get the primitive type name from a wrapper type name. + * Returns the input unchanged if already primitive or not a wrapper. + */ + public static String getUnboxedName(String typeName) { + switch (typeName) { + case "Byte": return "byte"; + case "Short": return "short"; + case "Integer": return "int"; + case "Long": return "long"; + case "Float": return "float"; + case "Double": return "double"; + case "Character": return "char"; + case "Boolean": return "boolean"; + default: return typeName; + } + } + + /** + * Get the wrapper type name from a primitive type name. + * Returns the input unchanged if already a wrapper or not a primitive. + */ + public static String getBoxedName(String typeName) { + switch (typeName) { + case "byte": return "Byte"; + case "short": return "Short"; + case "int": return "Integer"; + case "long": return "Long"; + case "float": return "Float"; + case "double": return "Double"; + case "char": return "Character"; + case "boolean": return "Boolean"; + default: return typeName; + } + } + + /** + * Check if the type name represents a void type. + */ + public static boolean isVoidType(String typeName) { + return typeName == null || typeName.equals("void") || typeName.equals("Void"); + } + + /** + * Check if the type represents a void type. + */ + public static boolean isVoidType(TypeInfo type) { + if (type == null) return true; + return isVoidType(type.getSimpleName()); + } +} + diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 9f342c9d6..1559a3b04 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -204,6 +204,50 @@ private void extractErrors(Token token) { if (fieldInfo != null && !fieldInfo.isResolved()) { errors.add("Cannot resolve symbol '" + token.getText() + "'"); } + + // Show method declaration errors (missing return, parameter errors, return type errors) + MethodInfo methodDecl = findMethodDeclarationContainingPosition(token); + if (methodDecl != null && methodDecl.hasError()) { + int tokenStart = token.getGlobalStart(); + int tokenEnd = token.getGlobalEnd(); + + // If hovering over the method name, show missing return error + if (methodDecl.hasMissingReturnError()) { + int methodNameStart = methodDecl.getNameOffset(); + int methodNameEnd = methodNameStart + methodDecl.getName().length(); + + if (tokenStart >= methodNameStart && tokenEnd <= methodNameEnd) { + errors.add(methodDecl.getErrorMessage()); + } + } + + // If hovering over a parameter with an error, show that error + if (methodDecl.hasParameterErrors()) { + for (MethodInfo.ParameterError paramError : methodDecl.getParameterErrors()) { + FieldInfo param = paramError.getParameter(); + if (param != null && param.getDeclarationOffset() >= 0) { + int paramStart = param.getDeclarationOffset(); + int paramEnd = paramStart + param.getName().length(); + + if (tokenStart >= paramStart && tokenEnd <= paramEnd) { + errors.add(paramError.getMessage()); + } + } + } + } + + // If hovering over a return statement with a type error, show that error + if (methodDecl.hasReturnStatementErrors()) { + for (MethodInfo.ReturnStatementError returnError : methodDecl.getReturnStatementErrors()) { + int returnStart = returnError.getStartOffset(); + int returnEnd = returnError.getEndOffset(); + + if (tokenStart >= returnStart && tokenEnd <= returnEnd) { + errors.add(returnError.getMessage()); + } + } + } + } } /** @@ -247,6 +291,36 @@ private MethodCallInfo findMethodCallContainingPosition(Token token) { } return null; } + + /** + * Find the method declaration that contains this token's position. + * Returns null if the token is not within a method declaration (header or body). + */ + private MethodInfo findMethodDeclarationContainingPosition(Token token) { + ScriptLine line = token.getParentLine(); + if (line == null || line.getParent() == null) { + return null; + } + + ScriptDocument doc = line.getParent(); + int tokenStart = token.getGlobalStart(); + + for (MethodInfo method : doc.getMethods()) { + if (!method.isDeclaration()) + continue; + + // Check if token is within the method declaration header OR body + int methodStart = method.getFullDeclarationOffset(); + if (methodStart < 0) methodStart = method.getTypeOffset(); + int bodyEnd = method.getBodyEnd(); + + // Token is within the method (header + body) + if (tokenStart >= methodStart && tokenStart <= bodyEnd) { + return method; + } + } + return null; + } /** * Find the argument that contains the given position. From 6d20f5f52cb597ba02f08b2c02537b3ff9ef2f15 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 19:04:14 +0200 Subject: [PATCH 080/337] Added error "Cannot return a value from a method with void result type" --- .../util/script/interpreter/MethodInfo.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 5614c2fec..484155472 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -29,6 +29,7 @@ public enum ErrorType { NONE, MISSING_RETURN, // Non-void method missing return statement RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type + VOID_METHOD_RETURNS_VALUE, // Void method returns a value DUPLICATE_PARAMETER, // Two parameters have the same name PARAMETER_UNDEFINED // Parameter type cannot be resolved } @@ -454,9 +455,9 @@ private void validateReturnStatement(String bodyText) { */ private void validateReturnTypes(String bodyText, TypeResolver typeResolver) { if (bodyText == null || bodyText.isEmpty()) return; - if (TypeChecker.isVoidType(returnType)) return; - String expectedTypeName = returnType.getSimpleName(); + boolean isVoid = TypeChecker.isVoidType(returnType); + String expectedTypeName = isVoid ? "void" : returnType.getSimpleName(); // Remove comments but keep strings (we need accurate positions) String cleanBody = CodeParser.removeComments(bodyText); @@ -478,12 +479,17 @@ private void validateReturnTypes(String bodyText, TypeResolver typeResolver) { // Extract the return expression String returnExpr = cleanBody.substring(returnPos + 6, semiPos).trim(); - // Skip empty returns (handled by missing return check) - if (!returnExpr.isEmpty()) { - // Calculate absolute position (bodyStart + 1 is after the opening brace) - int absoluteReturnStart = bodyStart + 1 + returnPos; - int absoluteSemiEnd = bodyStart + 1 + semiPos + 1; // +1 to include semicolon - + // Calculate absolute position (bodyStart + 1 is after the opening brace) + int absoluteReturnStart = bodyStart + 1 + returnPos; + int absoluteSemiEnd = bodyStart + 1 + semiPos + 1; // +1 to include semicolon + + // Check void method returning a value + if (isVoid && !returnExpr.isEmpty()) { + String message = "Cannot return a value from a method with void result type"; + addReturnStatementError(absoluteReturnStart, absoluteSemiEnd, message, returnType, null); + } + // Check non-void method return type compatibility + else if (!isVoid && !returnExpr.isEmpty() && typeResolver != null) { // Resolve the expression type TypeInfo actualType = typeResolver.resolveExpression(returnExpr, absoluteReturnStart); From e2a5d254a9cc38f7279a7efce1df90eb66103ace Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 19:41:28 +0200 Subject: [PATCH 081/337] Added error "method signature already defined in this scope" --- .../util/script/interpreter/MethodInfo.java | 27 ++++++ .../script/interpreter/MethodSignature.java | 79 +++++++++++++++ .../script/interpreter/ScriptDocument.java | 95 ++++++++++++++++++- .../util/script/interpreter/ScriptLine.java | 41 ++++++++ .../interpreter/hover/TokenHoverInfo.java | 10 ++ 5 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodSignature.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java index 484155472..5aa7da676 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java @@ -30,6 +30,7 @@ public enum ErrorType { MISSING_RETURN, // Non-void method missing return statement RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type VOID_METHOD_RETURNS_VALUE, // Void method returns a value + DUPLICATE_METHOD, // Method with same signature already defined in scope DUPLICATE_PARAMETER, // Two parameters have the same name PARAMETER_UNDEFINED // Parameter type cannot be resolved } @@ -191,6 +192,16 @@ public boolean containsPosition(int position) { return position >= bodyStart && position < bodyEnd; } + /** + * Get the end of the method declaration (closing paren position). + * This is used for error highlighting of duplicate methods. + */ + public int getDeclarationEnd() { + // The declaration ends just before the opening brace + // We find the closing paren by searching backwards from bodyStart + return bodyStart > 0 ? bodyStart - 1 : nameOffset + name.length(); + } + /** * Check if this method has a parameter with the given name. */ @@ -215,6 +226,22 @@ public FieldInfo getParameter(String paramName) { return null; } + /** + * Get method signature for duplicate detection. + * Signature = name + parameter types (ignoring parameter names). + */ + public MethodSignature cachedSignature; + + public MethodSignature getSignature() { + if (cachedSignature == null) { + List paramTypes = new ArrayList<>(); + for (FieldInfo param : parameters) + paramTypes.add(param.getDeclaredType()); + cachedSignature = new MethodSignature(name, paramTypes); + } + return cachedSignature; + } + /** * Get the appropriate TokenType for highlighting this method. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodSignature.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodSignature.java new file mode 100644 index 000000000..ed63d2925 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodSignature.java @@ -0,0 +1,79 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a method signature for robust comparison. + * Compares method name and parameter types as structured data, not as strings. + */ +public final class MethodSignature { + private final String methodName; + private final List parameterTypes; + + public MethodSignature(String methodName, List parameterTypes) { + this.methodName = methodName; + this.parameterTypes = new ArrayList<>(parameterTypes); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof MethodSignature)) + return false; + + MethodSignature other = (MethodSignature) obj; + + // Compare method names + if (!this.methodName.equals(other.methodName)) + return false; + + // Compare parameter count + if (this.parameterTypes.size() != other.parameterTypes.size()) + return false; + + // Compare each parameter type + for (int i = 0; i < this.parameterTypes.size(); i++) { + TypeInfo thisType = this.parameterTypes.get(i); + TypeInfo otherType = other.parameterTypes.get(i); + + // Both null = equal + if (thisType == null && otherType == null) + continue; + + // One null, one not = not equal + if (thisType == null || otherType == null) + return false; + + // Compare full type names + if (!thisType.getFullName().equals(otherType.getFullName())) + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = methodName.hashCode(); + for (TypeInfo paramType : parameterTypes) { + result = 31 * result + (paramType != null ? paramType.getFullName().hashCode() : 0); + } + return result; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(methodName); + sb.append("("); + for (int i = 0; i < parameterTypes.size(); i++) { + if (i > 0) + sb.append(", "); + TypeInfo type = parameterTypes.get(i); + sb.append(type != null ? type.getFullName() : "?"); + } + sb.append(")"); + return sb.toString(); + } +} 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 73874410c..89de53502 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 @@ -1,6 +1,5 @@ package noppes.npcs.client.gui.util.script.interpreter; -import net.minecraft.client.Minecraft; import noppes.npcs.client.ClientProxy; import java.util.*; @@ -604,6 +603,9 @@ private void parseMethodDeclarations() { methods.add(methodInfo); } + // Check for duplicate method declarations + checkDuplicateMethods(); + // Also parse JS-style functions Matcher funcM = FUNC_PARAMS_PATTERN.matcher(text); while (funcM.find()) { @@ -666,6 +668,97 @@ private int findFullDeclarationStart(int typeStart, String sourceText) { return earliestPos; } + /** + * Check for duplicate method declarations with same signature in same scope. + */ + private void checkDuplicateMethods() { + // Group methods by scope depth + Map> methodsByScope = new HashMap<>(); + + for (MethodInfo method : methods) { + int scopeDepth = calculateScopeDepth(method.getFullDeclarationOffset()); + methodsByScope.computeIfAbsent(scopeDepth, k -> new ArrayList<>()).add(method); + } + + // Check each scope for duplicates + for (List scopeMethods : methodsByScope.values()) { + Map> signatureMap = new HashMap<>(); + + for (MethodInfo method : scopeMethods) { + MethodSignature signature = method.getSignature(); + signatureMap.computeIfAbsent(signature, k -> new ArrayList<>()).add(method); + } + + // Mark all methods with duplicate signatures + for (Map.Entry> entry : signatureMap.entrySet()) { + List duplicates = entry.getValue(); + if (duplicates.size() > 1) { + for (MethodInfo duplicate : duplicates) { + duplicate.setError( + MethodInfo.ErrorType.DUPLICATE_METHOD, + duplicate.getSignature() + " is already defined in this scope" + ); + } + } + } + } + } + + /** + * Calculate the scope depth of a position (0 = top level, 1 = in class, etc.). + */ + private int calculateScopeDepth(int position) { + int depth = 0; + int braceDepth = 0; + boolean inString = false; + boolean inComment = false; + boolean inLineComment = false; + + for (int i = 0; i < position && i < text.length(); i++) { + char c = text.charAt(i); + char next = (i + 1 < text.length()) ? text.charAt(i + 1) : 0; + + // Handle strings + if (c == '"' && !inComment && !inLineComment) { + inString = !inString; + continue; + } + if (inString) continue; + + // Handle comments + if (!inComment && !inLineComment && c == '/' && next == '/') { + inLineComment = true; + i++; + continue; + } + if (!inComment && !inLineComment && c == '/' && next == '*') { + inComment = true; + i++; + continue; + } + if (inComment && c == '*' && next == '/') { + inComment = false; + i++; + continue; + } + if (inLineComment && c == '\n') { + inLineComment = false; + continue; + } + if (inComment || inLineComment) continue; + + // Track braces + if (c == '{') { + braceDepth++; + depth = Math.max(depth, braceDepth); + } else if (c == '}') { + braceDepth--; + } + } + + return depth; + } + private List parseParametersWithPositions(String paramList, int paramListStart) { List params = new ArrayList<>(); if (paramList == null || paramList.trim().isEmpty()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 455465c2c..69dcc33e1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -522,6 +522,47 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { } } + // Handle duplicate method error (underline from full declaration start to closing paren) + if (method.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) { + int declStart = method.getFullDeclarationOffset(); + int declEnd = method.getDeclarationEnd(); + + // Skip if declaration doesn't intersect this line + if (declEnd < lineStart || declStart > lineEnd) + continue; + + // Clip to line boundaries + int clipStart = Math.max(declStart, lineStart); + int clipEnd = Math.min(declEnd, lineEnd); + + if (clipStart >= clipEnd) + continue; + + // Convert to line-local coordinates + int lineLocalStart = clipStart - lineStart; + int lineLocalEnd = clipEnd - lineStart; + + // Bounds check + if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) + continue; + + // Compute pixel position + String beforeDecl = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeDecl); + + int declWidth; + if (lineLocalEnd > lineText.length()) { + declWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); + } else { + String declTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); + declWidth = ClientProxy.Font.width(declTextOnLine); + } + + if (declWidth > 0) { + drawCurlyUnderline(lineStartX + beforeWidth, baselineY, declWidth, 0xFF5555); + } + } + // Handle parameter errors (underline specific parameters) if (method.hasParameterErrors()) { for (MethodInfo.ParameterError paramError : method.getParameterErrors()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 1559a3b04..9db05c80f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -221,6 +221,16 @@ private void extractErrors(Token token) { } } + // If hovering over duplicate method declaration, show the error + if (methodDecl.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) { + int declStart = methodDecl.getFullDeclarationOffset(); + int declEnd = methodDecl.getDeclarationEnd(); + + if (tokenStart >= declStart && tokenEnd <= declEnd) { + errors.add(methodDecl.getErrorMessage()); + } + } + // If hovering over a parameter with an error, show that error if (methodDecl.hasParameterErrors()) { for (MethodInfo.ParameterError paramError : methodDecl.getParameterErrors()) { From 534ec7b2c52dacd1b92ccc33373fab7001697cd7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 19:57:10 +0200 Subject: [PATCH 082/337] Refactored error curly underline rendering to its own helper class --- .../interpreter/ErrorUnderlineRenderer.java | 272 ++++++++++++ .../util/script/interpreter/ScriptLine.java | 397 +----------------- 2 files changed, 281 insertions(+), 388 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java new file mode 100644 index 000000000..d396c149d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java @@ -0,0 +1,272 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.ClientProxy; +import org.lwjgl.opengl.GL11; + +/** + * Helper class for rendering error underlines in script editor lines. + * Handles the repetitive calculation of underline positions and drawing of curly underlines. + */ +public class ErrorUnderlineRenderer { + private static final int ERROR_COLOR = 0xFF5555; + + // Holds the calculation result for an underline + private static class UnderlinePosition { + final int x; + final int width; + + UnderlinePosition(int x, int width) { + this.x = x; + this.width = width; + } + + boolean isValid() { + return width > 0; + } + } + + /** + * Draw error underlines for all validation errors in the document that intersect the given line. + * + * @param doc The script document containing all errors + * @param lineStartX X coordinate where the line starts rendering + * @param baselineY Y coordinate for the underline baseline + * @param lineText The text content of the line + * @param lineStart Global offset where this line starts + * @param lineEnd Global offset where this line ends + */ + public static void drawErrorUnderlines( + ScriptDocument doc, + int lineStartX, int baselineY, + String lineText, int lineStart, int lineEnd) { + + if (doc == null) + return; + + + // Check all method calls in the document + for (MethodCallInfo call : doc.getMethodCalls()) { + // Skip method declarations that were erroneously recorded as calls + boolean isDeclaration = false; + int methodStart = call.getMethodNameStart(); + for (MethodInfo mi : doc.getMethods()) { + if (mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) { + isDeclaration = true; + break; + } + } + if (isDeclaration) + continue; + + // Skip if this call doesn't intersect this line + if (call.getCloseParenOffset() < lineStart || call.getOpenParenOffset() > lineEnd) + continue; + + // Handle arg count errors (underline the method name) + if (call.hasArgCountError()) { + int methodEnd = methodStart + call.getMethodName().length(); + drawUnderlineForSpan(methodStart, methodEnd, lineStartX, baselineY, + lineText, lineStart, lineEnd, ERROR_COLOR); + } + + // Handle return type mismatch (underline the method name) - currently commented out + // if (call.hasReturnTypeMismatch()) { ... } + + // Handle arg type errors (underline specific arguments) + if (call.hasArgTypeError()) { + for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) { + MethodCallInfo.Argument arg = error.getArg(); + drawUnderlineForSpan(arg.getStartOffset(), arg.getEndOffset(), + lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); + } + } + } + + // Check all field accesses in the document (currently commented out) + // for (FieldAccessInfo access : doc.getFieldAccesses()) { ... } + + // Check all errored assignments in the document + for (AssignmentInfo assign : doc.getAllErroredAssignments()) { + int underlineStart, underlineEnd; + + if (assign.isLhsError()) { + underlineStart = assign.getLhsStart(); + underlineEnd = assign.getLhsEnd(); + } else if (assign.isRhsError()) { + underlineStart = assign.getRhsStart(); + underlineEnd = assign.getRhsEnd(); + } else if (assign.isFullLineError()) { + underlineStart = assign.getStatementStart(); + underlineEnd = assign.getRhsEnd(); + } else { + continue; + } + + drawUnderlineForSpan(underlineStart, underlineEnd, lineStartX, baselineY, + lineText, lineStart, lineEnd, ERROR_COLOR); + } + + // Check all method declarations for errors + for (MethodInfo method : doc.getMethods()) { + if (!method.isDeclaration() || !method.hasError()) + continue; + + // Handle return statement type errors + if (method.hasReturnStatementErrors()) { + for (MethodInfo.ReturnStatementError returnError : method.getReturnStatementErrors()) { + drawUnderlineForSpan(returnError.getStartOffset(), returnError.getEndOffset(), + lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); + } + } + + // Handle missing return error (underline the method name) + if (method.hasMissingReturnError()) { + int methodNameStart = method.getNameOffset(); + int methodNameEnd = methodNameStart + method.getName().length(); + drawUnderlineForSpan(methodNameStart, methodNameEnd, lineStartX, baselineY, + lineText, lineStart, lineEnd, ERROR_COLOR); + } + + // Handle duplicate method error (underline from full declaration start to closing paren) + if (method.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) { + drawUnderlineForSpan(method.getFullDeclarationOffset(), method.getDeclarationEnd(), + lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); + } + + // Handle parameter errors + if (method.hasParameterErrors()) { + for (MethodInfo.ParameterError paramError : method.getParameterErrors()) { + FieldInfo param = paramError.getParameter(); + if (param == null || param.getDeclarationOffset() < 0) + continue; + + int paramStart = param.getDeclarationOffset(); + int paramEnd = paramStart + param.getName().length(); + drawUnderlineForSpan(paramStart, paramEnd, lineStartX, baselineY, + lineText, lineStart, lineEnd, ERROR_COLOR); + } + } + } + } + + /** + * Calculate underline position for a span of text within a line. + * Handles clipping to line boundaries and pixel width calculation. + * + * @param spanStart Global offset where the span starts + * @param spanEnd Global offset where the span ends + * @param lineStartX X coordinate where the line starts rendering + * @param lineText The text content of the line + * @param lineStart Global offset where this line starts + * @param lineEnd Global offset where this line ends + * @return UnderlinePosition with x and width, or null if span doesn't intersect line + */ + private static UnderlinePosition calculateUnderlinePosition( + int spanStart, int spanEnd, + int lineStartX, String lineText, int lineStart, int lineEnd) { + + // Skip if span doesn't intersect this line + if (spanEnd < lineStart || spanStart > lineEnd) + return null; + + // Clip to line boundaries + int clipStart = Math.max(spanStart, lineStart); + int clipEnd = Math.min(spanEnd, lineEnd); + + if (clipStart >= clipEnd) + return null; + + // Convert to line-local coordinates + int lineLocalStart = clipStart - lineStart; + int lineLocalEnd = clipEnd - lineStart; + + // Bounds check + if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) + return null; + + // Compute pixel position + String beforeSpan = lineText.substring(0, lineLocalStart); + int beforeWidth = ClientProxy.Font.width(beforeSpan); + + int spanWidth; + if (lineLocalEnd > lineText.length()) { + // Span extends past line end + spanWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); + } else { + // Span is fully on this line (or clipped) + String spanTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); + spanWidth = ClientProxy.Font.width(spanTextOnLine); + } + + return new UnderlinePosition(lineStartX + beforeWidth, spanWidth); + } + + /** + * Draw an underline for a simple span if it intersects the line. + * + * @param spanStart Global offset where the span starts + * @param spanEnd Global offset where the span ends + * @param lineStartX X coordinate where the line starts rendering + * @param baselineY Y coordinate for the underline baseline + * @param lineText The text content of the line + * @param lineStart Global offset where this line starts + * @param lineEnd Global offset where this line ends + * @param color Underline color + */ + public static void drawUnderlineForSpan( + int spanStart, int spanEnd, + int lineStartX, int baselineY, + String lineText, int lineStart, int lineEnd, + int color) { + + UnderlinePosition pos = calculateUnderlinePosition( + spanStart, spanEnd, lineStartX, lineText, lineStart, lineEnd); + + if (pos != null && pos.isValid()) { + drawCurlyUnderline(pos.x, baselineY, pos.width, color); + } + } + + /** + * Draw a curly/wavy underline (like IDE error highlighting). + * @param x Start X position + * @param y Y position (bottom of text) + * @param width Width of the underline + * @param color Color in ARGB format (e.g., 0xFFFF5555 for red) + */ + public static void drawCurlyUnderline(int x, int y, int width, int color) { + if (width <= 0) + return; + + float a = ((color >> 24) & 0xFF) / 255f; + float r = ((color >> 16) & 0xFF) / 255f; + float g = ((color >> 8) & 0xFF) / 255f; + float b = (color & 0xFF) / 255f; + + // If alpha is 0, assume full opacity + if (a == 0) + a = 1.0f; + + GL11.glPushMatrix(); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + GL11.glLineWidth(1.0f); + + GL11.glBegin(GL11.GL_LINE_STRIP); + // Wave parameters: 2 pixels amplitude, 4 pixels wavelength + int waveHeight = 1; + float waveLength = 4f; + for (float i = -0.5f; i <= width - 1; i += 0.125f) { + // Create a sine-like wave pattern + double phase = (double) i / waveLength * Math.PI * 2; + float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.25f; + GL11.glVertex2f(x + i + 2f, y + yOffset); + } + GL11.glEnd(); + + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopMatrix(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 69dcc33e1..2f10b2546 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -261,399 +261,20 @@ public void drawString(int x, int y, int defaultColor) { /** * Draw underlines for all validation errors (method calls and field accesses) that intersect this line. - * Uses the same token-walking logic as Token.drawUnderline but applies it - * to ALL validatable items in the document, enabling multi-line underlines. + * Delegates to ErrorUnderlineRenderer for the actual rendering logic. */ private void drawErrorUnderlines(int lineStartX, int baselineY) { if (parent == null) return; - ScriptDocument doc = parent; - String lineText = getText(); - int lineStart = getGlobalStart(); - int lineEnd = getGlobalEnd(); - - // Check all method calls in the document - for (MethodCallInfo call : doc.getMethodCalls()) { - // Skip method declarations that were erroneously recorded as calls. - boolean isDeclaration = false; - int methodStart = call.getMethodNameStart(); - for (MethodInfo mi : doc.getMethods()) { - //SKIP method declarations cause for some reason their underlines are drawn here - if (mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) { - isDeclaration = true; - break; - } - } - if (isDeclaration) - continue; - // Skip if this call doesn't intersect this line - if (call.getCloseParenOffset() < lineStart || call.getOpenParenOffset() > lineEnd) - continue; - - // Handle arg count errors (underline the method name) - if (call.hasArgCountError()) { - // Check if method name is on this line - if (methodStart >= lineStart && methodStart < lineEnd) { - int lineLocalStart = methodStart - lineStart; - if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { - String beforeMethod = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeMethod); - int methodWidth = ClientProxy.Font.width(call.getMethodName()); - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); - } - } - } - - // Handle return type mismatch (underline the method name) - if (call.hasReturnTypeMismatch()) { - // Check if method name is on this line - if (methodStart >= lineStart && methodStart < lineEnd) { - int lineLocalStart = methodStart - lineStart; - if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { - String beforeMethod = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeMethod); - int methodWidth = ClientProxy.Font.width(call.getMethodName()); - // drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); - } - } - } - - // Handle arg type errors (underline specific arguments) - if (call.hasArgTypeError()) { - for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) { - MethodCallInfo.Argument arg = error.getArg(); - int argStart = arg.getStartOffset(); - int argEnd = arg.getEndOffset(); - - // Clip argument to this line's bounds - int clipStart = Math.max(argStart, lineStart); - int clipEnd = Math.min(argEnd, lineEnd); - - // Skip if argument doesn't intersect this line - if (clipStart >= clipEnd) - continue; - - // Convert to line-local coordinates - int lineLocalStart = clipStart - lineStart; - int lineLocalEnd = clipEnd - lineStart; - - // Bounds check - if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) - continue; - - // Compute pixel position using same logic as Token.drawUnderline - String beforeArg = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeArg); - - int argWidth; - if (lineLocalEnd > lineText.length()) { - // Argument extends past line end - argWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); - } else { - // Argument is fully on this line (or clipped) - String argTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); - argWidth = ClientProxy.Font.width(argTextOnLine); - } - - if (argWidth > 0) { - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, argWidth, 0xFF5555); - } - } - } - } - - // Check all field accesses in the document - for (FieldAccessInfo access : doc.getFieldAccesses()) { - // Skip if this access doesn't intersect this line - int fieldStart = access.getFieldNameStart(); - int fieldEnd = access.getFieldNameEnd(); - - if (fieldEnd < lineStart || fieldStart > lineEnd) - continue; - - // Handle type mismatch errors (underline the field name) - if (access.hasTypeMismatch()) { - // Check if field name is on this line - if (fieldStart >= lineStart && fieldStart < lineEnd) { - int lineLocalStart = fieldStart - lineStart; - if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { - String beforeField = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeField); - int fieldWidth = ClientProxy.Font.width(access.getFieldName()); - // drawCurlyUnderline(lineStartX + beforeWidth, baselineY, fieldWidth, 0xFF5555); - } - } - } - } - - // Check all errored assignments in the document (stored in FieldInfo objects) - for (AssignmentInfo assign : doc.getAllErroredAssignments()) { - // Determine what to underline based on error type - // LHS errors (final reassignment, access errors) underline the LHS - // RHS errors (type mismatch) underline the RHS - - int underlineStart, underlineEnd; - String underlineText; - - if (assign.isLhsError()) { - // Underline the LHS (target variable) - underlineStart = assign.getLhsStart(); - underlineEnd = assign.getLhsEnd(); - underlineText = assign.getTargetName(); - } else if (assign.isRhsError()) { - // Underline the RHS (source expression) - underlineStart = assign.getRhsStart(); - underlineEnd = assign.getRhsEnd(); - underlineText = assign.getSourceExpr(); - } else if (assign.isFullLineError()) { - underlineStart = assign.getStatementStart(); - underlineEnd = assign.getRhsEnd(); - } else { - // Skip assignments with no displayable error - continue; - } - - // Skip if this doesn't intersect this line - if (underlineEnd < lineStart || underlineStart > lineEnd) - continue; - - // Clip to line boundaries - int clipStart = Math.max(underlineStart, lineStart); - int clipEnd = Math.min(underlineEnd, lineEnd); - - if (clipStart >= clipEnd) - continue; - - // Convert to line-local coordinates - int lineLocalStart = clipStart - lineStart; - int lineLocalEnd = clipEnd - lineStart; - - // Bounds check - if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) - continue; - - // Compute pixel position - String beforeUnderline = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeUnderline); - - int underlineWidth; - if (lineLocalEnd > lineText.length()) { - // Extends past line end - underlineWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); - } else { - String underlineOnLine = lineText.substring(lineLocalStart, lineLocalEnd); - underlineWidth = ClientProxy.Font.width(underlineOnLine); - } - - if (underlineWidth > 0) { - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, underlineWidth, 0xFF5555); - } - } - - // Check all method declarations for errors (missing return, parameter errors, return type errors) - for (MethodInfo method : doc.getMethods()) { - if (!method.isDeclaration() || !method.hasError()) - continue; - - // Handle return statement type errors (underline the entire return statement) - if (method.hasReturnStatementErrors()) { - for (MethodInfo.ReturnStatementError returnError : method.getReturnStatementErrors()) { - int returnStart = returnError.getStartOffset(); - int returnEnd = returnError.getEndOffset(); - - //error 247, 258 - //line 235, 259 - // Skip if return statement doesn't intersect this line - if (returnEnd < lineStart || returnStart > lineEnd) - continue; - - // Clip to line boundaries - int clipStart = Math.max(returnStart, lineStart); - int clipEnd = Math.min(returnEnd, lineEnd); - - if (clipStart >= clipEnd) - continue; - - // Convert to line-local coordinates - int lineLocalStart = clipStart - lineStart; - int lineLocalEnd = clipEnd - lineStart; - - // Bounds check - if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) - continue; - - // Compute pixel position - String beforeReturn = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeReturn); - - int returnWidth; - if (lineLocalEnd > lineText.length()) { - returnWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); - } else { - String returnTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); - returnWidth = ClientProxy.Font.width(returnTextOnLine); - } - - if (returnWidth > 0) { - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, returnWidth, 0xFF5555); - } - } - } - - // Handle missing return error (underline the method name) - if (method.hasMissingReturnError()) { - int methodNameStart = method.getNameOffset(); - int methodNameEnd = methodNameStart + method.getName().length(); - - // Skip if method name doesn't intersect this line - if (methodNameEnd < lineStart || methodNameStart > lineEnd) - continue; - - // Check if method name is on this line - if (methodNameStart >= lineStart && methodNameStart < lineEnd) { - int lineLocalStart = methodNameStart - lineStart; - if (lineLocalStart >= 0 && lineLocalStart < lineText.length()) { - String beforeMethod = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeMethod); - int methodWidth = ClientProxy.Font.width(method.getName()); - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, methodWidth, 0xFF5555); - } - } - } - - // Handle duplicate method error (underline from full declaration start to closing paren) - if (method.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) { - int declStart = method.getFullDeclarationOffset(); - int declEnd = method.getDeclarationEnd(); - - // Skip if declaration doesn't intersect this line - if (declEnd < lineStart || declStart > lineEnd) - continue; - - // Clip to line boundaries - int clipStart = Math.max(declStart, lineStart); - int clipEnd = Math.min(declEnd, lineEnd); - - if (clipStart >= clipEnd) - continue; - - // Convert to line-local coordinates - int lineLocalStart = clipStart - lineStart; - int lineLocalEnd = clipEnd - lineStart; - - // Bounds check - if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) - continue; - - // Compute pixel position - String beforeDecl = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeDecl); - - int declWidth; - if (lineLocalEnd > lineText.length()) { - declWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); - } else { - String declTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); - declWidth = ClientProxy.Font.width(declTextOnLine); - } - - if (declWidth > 0) { - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, declWidth, 0xFF5555); - } - } - - // Handle parameter errors (underline specific parameters) - if (method.hasParameterErrors()) { - for (MethodInfo.ParameterError paramError : method.getParameterErrors()) { - FieldInfo param = paramError.getParameter(); - if (param == null || param.getDeclarationOffset() < 0) - continue; - - int paramStart = param.getDeclarationOffset(); - int paramEnd = paramStart + param.getName().length(); - - // Skip if parameter doesn't intersect this line - if (paramEnd < lineStart || paramStart > lineEnd) - continue; - - // Clip to line boundaries - int clipStart = Math.max(paramStart, lineStart); - int clipEnd = Math.min(paramEnd, lineEnd); - - if (clipStart >= clipEnd) - continue; - - // Convert to line-local coordinates - int lineLocalStart = clipStart - lineStart; - int lineLocalEnd = clipEnd - lineStart; - - // Bounds check - if (lineLocalStart < 0 || lineLocalStart >= lineText.length()) - continue; - - // Compute pixel position - String beforeParam = lineText.substring(0, lineLocalStart); - int beforeWidth = ClientProxy.Font.width(beforeParam); - - int paramWidth; - if (lineLocalEnd > lineText.length()) { - paramWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart)); - } else { - String paramTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd); - paramWidth = ClientProxy.Font.width(paramTextOnLine); - } - - if (paramWidth > 0) { - drawCurlyUnderline(lineStartX + beforeWidth, baselineY, paramWidth, 0xFF5555); - } - } - } - - } - } - - /** - * Draw a curly/wavy underline (like IDE error highlighting). - * @param x Start X position - * @param y Y position (bottom of text) - * @param width Width of the underline - * @param color Color in ARGB format (e.g., 0xFFFF5555 for red) - */ - protected static void drawCurlyUnderline(int x, int y, int width, int color) { - if (width <= 0) - return; - - float a = ((color >> 24) & 0xFF) / 255f; - float r = ((color >> 16) & 0xFF) / 255f; - float g = ((color >> 8) & 0xFF) / 255f; - float b = (color & 0xFF) / 255f; - - // If alpha is 0, assume full opacity - if (a == 0) - a = 1.0f; - - GL11.glPushMatrix(); - GL11.glDisable(GL11.GL_TEXTURE_2D); - GL11.glEnable(GL11.GL_BLEND); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GL11.glColor4f(r, g, b, a); - GL11.glLineWidth(1.0f); - - GL11.glBegin(GL11.GL_LINE_STRIP); - // Wave parameters: 2 pixels amplitude, 4 pixels wavelength - int waveHeight = 1; - float waveLength = 4f; - for (float i = -0.5f; i <= width - 1; i += 0.125f) { - // Create a sine-like wave pattern - double phase = (double) i / waveLength * Math.PI * 2; - float yOffset = (float) (Math.sin(phase) * waveHeight) - 0.25f; - GL11.glVertex2f(x + i + 2f, y + yOffset); - } - GL11.glEnd(); - - GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glPopMatrix(); + ErrorUnderlineRenderer.drawErrorUnderlines( + parent, + lineStartX, + baselineY, + getText(), + getGlobalStart(), + getGlobalEnd() + ); } /** From c2e0bbedae6e3e98ad314469cf62770f778bd432 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 20:33:39 +0200 Subject: [PATCH 083/337] Small package refactor --- .../client/gui/util/GuiScriptTextArea.java | 4 +- .../interpreter/ErrorUnderlineRenderer.java | 4 ++ .../script/interpreter/ScriptDocument.java | 12 +++++ .../util/script/interpreter/ScriptLine.java | 9 +++- .../interpreter/ScriptTextContainer.java | 47 ++++++++++--------- .../{ => field}/AssignmentInfo.java | 4 +- .../{ => field}/FieldAccessInfo.java | 4 +- .../interpreter/{ => field}/FieldInfo.java | 7 ++- .../script/interpreter/hover/HoverState.java | 2 +- .../interpreter/hover/TokenHoverInfo.java | 10 ++++ .../{ => method}/MethodCallInfo.java | 6 ++- .../interpreter/{ => method}/MethodInfo.java | 8 +++- .../{ => method}/MethodSignature.java | 4 +- .../script/interpreter/{ => token}/Token.java | 10 +++- .../interpreter/{ => token}/TokenType.java | 2 +- .../interpreter/{ => type}/ImportData.java | 4 +- .../{ => type}/ScriptTypeInfo.java | 6 ++- .../interpreter/{ => type}/TypeChecker.java | 2 +- .../interpreter/{ => type}/TypeInfo.java | 6 ++- .../interpreter/{ => type}/TypeResolver.java | 3 +- 20 files changed, 113 insertions(+), 41 deletions(-) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => field}/AssignmentInfo.java (99%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => field}/FieldAccessInfo.java (97%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => field}/FieldInfo.java (97%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => method}/MethodCallInfo.java (98%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => method}/MethodInfo.java (98%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => method}/MethodSignature.java (94%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => token}/Token.java (93%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => token}/TokenType.java (98%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => type}/ImportData.java (97%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => type}/ScriptTypeInfo.java (96%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => type}/TypeChecker.java (98%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => type}/TypeInfo.java (97%) rename src/main/java/noppes/npcs/client/gui/util/script/interpreter/{ => type}/TypeResolver.java (99%) 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 d32e8fdf7..8c6ac7861 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -13,8 +13,8 @@ // New interpreter system imports import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; -import noppes.npcs.client.gui.util.script.interpreter.Token; -import noppes.npcs.client.gui.util.script.interpreter.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; import noppes.npcs.client.gui.util.script.interpreter.hover.TokenHoverRenderer; import noppes.npcs.client.key.impl.ScriptEditorKeys; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java index d396c149d..3f475452a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java @@ -1,6 +1,10 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import org.lwjgl.opengl.GL11; /** 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 89de53502..0367b159d 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 @@ -1,6 +1,18 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; +import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; +import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; import java.util.*; import java.util.regex.Matcher; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 2f10b2546..096df2e80 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -1,7 +1,14 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.ClientProxy; -import org.lwjgl.opengl.GL11; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.ArrayList; import java.util.Collections; 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 5c73fe530..f32bd4f7e 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 @@ -2,6 +2,8 @@ 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 java.util.ArrayList; import java.util.List; @@ -91,7 +93,7 @@ private void rebuildLineData() { ld.indentCols.addAll(scriptLine.getIndentGuides()); // Convert tokens - use fully qualified name to avoid conflict with inherited Token - for (noppes.npcs.client.gui.util.script.interpreter.Token interpreterToken : scriptLine.getTokens()) { + for (noppes.npcs.client.gui.util.script.interpreter.token.Token interpreterToken : scriptLine.getTokens()) { ld.tokens.add(new JavaTextContainer.Token( interpreterToken.getText(), toLegacyTokenType(interpreterToken.getType()), @@ -121,26 +123,27 @@ public void formatCodeText() { /** * Convert new interpreter TokenType to legacy JavaTextContainer.TokenType format. */ - private JavaTextContainer.TokenType toLegacyTokenType(noppes.npcs.client.gui.util.script.interpreter.TokenType type) { - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.COMMENT) return JavaTextContainer.TokenType.COMMENT; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.STRING) return JavaTextContainer.TokenType.STRING; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_KEYWORD) return JavaTextContainer.TokenType.CLASS_KEYWORD; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.IMPORT_KEYWORD) return JavaTextContainer.TokenType.IMPORT_KEYWORD; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.KEYWORD) return JavaTextContainer.TokenType.KEYWORD; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.MODIFIER) return JavaTextContainer.TokenType.MODIFIER; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.INTERFACE_DECL) return JavaTextContainer.TokenType.INTERFACE_DECL; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.ENUM_DECL) return JavaTextContainer.TokenType.ENUM_DECL; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.CLASS_DECL) return JavaTextContainer.TokenType.CLASS_DECL; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.IMPORTED_CLASS) return JavaTextContainer.TokenType.IMPORTED_CLASS; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.TYPE_DECL) return JavaTextContainer.TokenType.TYPE_DECL; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_DECL) return JavaTextContainer.TokenType.METHOD_DECARE; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.METHOD_CALL) return JavaTextContainer.TokenType.METHOD_CALL; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.LITERAL) return JavaTextContainer.TokenType.NUMBER; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.GLOBAL_FIELD) return JavaTextContainer.TokenType.GLOBAL_FIELD; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.LOCAL_FIELD) return JavaTextContainer.TokenType.LOCAL_FIELD; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.PARAMETER) return JavaTextContainer.TokenType.PARAMETER; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.UNDEFINED_VAR) return JavaTextContainer.TokenType.UNDEFINED_VAR; - if (type == noppes.npcs.client.gui.util.script.interpreter.TokenType.VARIABLE) return JavaTextContainer.TokenType.VARIABLE; + private JavaTextContainer.TokenType toLegacyTokenType( + noppes.npcs.client.gui.util.script.interpreter.token.TokenType type) { + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.COMMENT) return JavaTextContainer.TokenType.COMMENT; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.STRING) return JavaTextContainer.TokenType.STRING; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.CLASS_KEYWORD) return JavaTextContainer.TokenType.CLASS_KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.IMPORT_KEYWORD) return JavaTextContainer.TokenType.IMPORT_KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.KEYWORD) return JavaTextContainer.TokenType.KEYWORD; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.MODIFIER) return JavaTextContainer.TokenType.MODIFIER; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.INTERFACE_DECL) return JavaTextContainer.TokenType.INTERFACE_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.ENUM_DECL) return JavaTextContainer.TokenType.ENUM_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.CLASS_DECL) return JavaTextContainer.TokenType.CLASS_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.IMPORTED_CLASS) return JavaTextContainer.TokenType.IMPORTED_CLASS; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.TYPE_DECL) return JavaTextContainer.TokenType.TYPE_DECL; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.METHOD_DECL) return JavaTextContainer.TokenType.METHOD_DECARE; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.METHOD_CALL) return JavaTextContainer.TokenType.METHOD_CALL; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.LITERAL) return JavaTextContainer.TokenType.NUMBER; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.GLOBAL_FIELD) return JavaTextContainer.TokenType.GLOBAL_FIELD; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.LOCAL_FIELD) return JavaTextContainer.TokenType.LOCAL_FIELD; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.PARAMETER) return JavaTextContainer.TokenType.PARAMETER; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.UNDEFINED_VAR) return JavaTextContainer.TokenType.UNDEFINED_VAR; + if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.VARIABLE) return JavaTextContainer.TokenType.VARIABLE; return JavaTextContainer.TokenType.DEFAULT; } @@ -160,7 +163,7 @@ public ScriptDocument getDocument() { * @param globalPosition Position in the document text * @return The Token at that position, or null */ - public noppes.npcs.client.gui.util.script.interpreter.Token getInterpreterTokenAt(int globalPosition) { + public noppes.npcs.client.gui.util.script.interpreter.token.Token getInterpreterTokenAt(int globalPosition) { if (!USE_NEW_INTERPRETER || document == null) { return null; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java similarity index 99% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java index 35bcdadae..1a1700939 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/AssignmentInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java @@ -1,4 +1,6 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.field; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.lang.reflect.Field; import java.lang.reflect.Modifier; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java similarity index 97% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java index ffd07b045..740ae1f46 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldAccessInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java @@ -1,4 +1,6 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.field; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; /** * Metadata for field access validation. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java similarity index 97% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 7031f21fd..6fa9bd613 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -1,6 +1,9 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.field; -import scala.annotation.meta.field; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import java.lang.reflect.Field; import java.lang.reflect.Modifier; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java index 65e9d7c2d..9a0afe286 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/HoverState.java @@ -2,7 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; -import noppes.npcs.client.gui.util.script.interpreter.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.Token; /** * Manages the hover state for token tooltips in the script editor. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 9db05c80f..8e186d454 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -1,6 +1,16 @@ package noppes.npcs.client.gui.util.script.interpreter.hover; import noppes.npcs.client.gui.util.script.interpreter.*; +import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; import java.lang.reflect.Field; import java.lang.reflect.Method; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java similarity index 98% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index cc53ec2d4..21aac4e20 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -1,6 +1,8 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.method; -import scala.annotation.meta.param; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java similarity index 98% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 5aa7da676..adee7fb33 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -1,4 +1,10 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.method; + +import noppes.npcs.client.gui.util.script.interpreter.*; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.lang.reflect.Constructor; import java.lang.reflect.Method; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodSignature.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodSignature.java similarity index 94% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodSignature.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodSignature.java index ed63d2925..ba7d3e61a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/MethodSignature.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodSignature.java @@ -1,4 +1,6 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.method; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java similarity index 93% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index af4af8ac7..efd917dc7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -1,4 +1,12 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.token; + +import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; +import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; /** * Represents a single token in the source code with its type and metadata. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java similarity index 98% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index d3c619758..58368a876 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -1,4 +1,4 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.token; /** * Defines all token types for syntax highlighting with hex colors and priorities. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ImportData.java similarity index 97% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ImportData.java index d3fe10820..cda544a98 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ImportData.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ImportData.java @@ -1,4 +1,6 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.token.Token; import java.util.ArrayList; import java.util.Collections; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java similarity index 96% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index a2fba036d..9f1f04a53 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -1,4 +1,8 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java similarity index 98% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeChecker.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index 8e63507ae..3fbd293d4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -1,4 +1,4 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.type; /** * Utility class for checking type compatibility between types. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java similarity index 97% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 6216fe9f0..8c9adadd0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -1,4 +1,8 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java similarity index 99% rename from src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java rename to src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java index fb1c2d068..a494c54b2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/TypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeResolver.java @@ -1,6 +1,7 @@ -package noppes.npcs.client.gui.util.script.interpreter; +package noppes.npcs.client.gui.util.script.interpreter.type; import noppes.npcs.client.gui.util.script.PackageFinder; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import java.util.*; From 941cc5e3fd81e2fe61ffe19762df0f967e86ffa5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 22:36:46 +0200 Subject: [PATCH 084/337] Expression resolving/type resolution framework with operator parsing --- .../script/interpreter/ScriptDocument.java | 352 ++++++++++++++++++ .../expression/ExpressionNode.java | 232 ++++++++++++ .../expression/ExpressionParser.java | 331 ++++++++++++++++ .../expression/ExpressionToken.java | 60 +++ .../expression/ExpressionTokenizer.java | 152 ++++++++ .../expression/ExpressionTypeResolver.java | 167 +++++++++ .../interpreter/expression/OperatorType.java | 147 ++++++++ .../interpreter/expression/TypeRules.java | 157 ++++++++ 8 files changed, 1598 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java 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 0367b159d..7554c1000 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 @@ -1,6 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionNode; +import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionTypeResolver; import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; @@ -2236,6 +2238,7 @@ private boolean hasExcessivePrecision(String numLiteral, int maxDigits) { * - Mixed chains (a.b().c.d()) * - Static access (Class.field, Class.method()) * - New expressions (new Type()) + * - All Java operators (arithmetic, logical, bitwise, etc.) * * This is THE method that should be used for all type resolution needs. */ @@ -2246,6 +2249,11 @@ private TypeInfo resolveExpressionType(String expr, int position) { return null; } + // Check if expression contains operators - if so, use the full expression resolver + if (containsOperators(expr)) { + return resolveExpressionWithOperators(expr, position); + } + // Invalid expressions starting with brackets if (expr.startsWith("[") || expr.startsWith("]")) { return null; // Invalid syntax @@ -2531,6 +2539,350 @@ private TypeInfo resolveChainSegment(TypeInfo currentType, ChainSegment segment) } } + // ==================== OPERATOR EXPRESSION RESOLUTION ==================== + + /** + * Check if an expression contains operators that need advanced resolution. + * This is a quick heuristic check - it may have false positives for operators + * inside strings, but those are handled by the full parser. + */ + private boolean containsOperators(String expr) { + if (expr == null) return false; + + boolean inString = false; + boolean inChar = false; + + for (int i = 0; i < expr.length(); i++) { + char c = expr.charAt(i); + char next = (i + 1 < expr.length()) ? expr.charAt(i + 1) : 0; + + // Track string literals + if (c == '"' && !inChar) { + if (!inString) { + inString = true; + } else if (i > 0 && expr.charAt(i - 1) != '\\') { + inString = false; + } + continue; + } + + // Track char literals + if (c == '\'' && !inString) { + if (!inChar) { + inChar = true; + } else if (i > 0 && expr.charAt(i - 1) != '\\') { + inChar = false; + } + continue; + } + + // Skip content inside strings/chars + if (inString || inChar) continue; + + // Check for operators (excluding . which is used for member access) + switch (c) { + case '+': + // + is arithmetic unless it's unary at start or after operator + if (next == '+') return true; // ++ + if (next == '=') return true; // += + // Check if this is binary + by looking at previous non-whitespace + int prevIdx = i - 1; + while (prevIdx >= 0 && Character.isWhitespace(expr.charAt(prevIdx))) prevIdx--; + if (prevIdx >= 0) { + char prev = expr.charAt(prevIdx); + // If previous char is identifier char or ), this is binary + if (Character.isJavaIdentifierPart(prev) || prev == ')' || prev == ']' || Character.isDigit(prev)) { + return true; + } + } + break; + + case '-': + // - is arithmetic unless it's unary minus before a number + if (next == '-') return true; // -- + if (next == '=') return true; // -= + // Check if this is binary - by looking at previous non-whitespace + prevIdx = i - 1; + while (prevIdx >= 0 && Character.isWhitespace(expr.charAt(prevIdx))) prevIdx--; + if (prevIdx >= 0) { + char prev = expr.charAt(prevIdx); + // If previous char is identifier char or ), this is binary + if (Character.isJavaIdentifierPart(prev) || prev == ')' || prev == ']' || Character.isDigit(prev)) { + return true; + } + } + break; + + case '*': case '/': case '%': + return true; + + case '&': + if (next == '&' || next == '=') return true; + // Single & is bitwise AND + return true; + + case '|': + if (next == '|' || next == '=') return true; + // Single | is bitwise OR + return true; + + case '^': case '~': + return true; + + case '<': + // Could be < > <= >= << or generics + if (next == '<' || next == '=') return true; + // Check if this looks like relational (not generic type params) + // Generics typically follow a type name directly with no space + int nextNonSpace = i + 1; + while (nextNonSpace < expr.length() && Character.isWhitespace(expr.charAt(nextNonSpace))) nextNonSpace++; + if (nextNonSpace < expr.length() && !Character.isUpperCase(expr.charAt(nextNonSpace))) { + return true; + } + break; + + case '>': + if (next == '>' || next == '=') return true; + // Similar check for generics + break; + + case '!': + if (next == '=') return true; // != + // Standalone ! is logical NOT + return true; + + case '=': + if (next == '=') return true; // == + // Single = is assignment + return true; + + case '?': + // Ternary operator - but be careful of generics with ? + // If followed by :, it's definitely ternary + for (int j = i + 1; j < expr.length(); j++) { + if (expr.charAt(j) == ':') return true; + } + break; + } + } + + // Check for instanceof keyword + if (expr.contains(" instanceof ")) { + return true; + } + + return false; + } + + /** + * Resolve an expression that contains operators using the full expression parser. + * This handles all Java operators with proper precedence and type promotion rules. + */ + private TypeInfo resolveExpressionWithOperators(String expr, int position) { + // Create a context that bridges to ScriptDocument's existing resolution methods + ExpressionNode.TypeResolverContext context = createExpressionResolverContext(position); + + // Use the expression resolver to parse and resolve the type + ExpressionTypeResolver resolver = new ExpressionTypeResolver(context); + TypeInfo result = resolver.resolve(expr); + + // If the expression resolver couldn't resolve it, fall back to simple resolution + if (result == null || !result.isResolved()) { + // Try the simple path in case the operator detection was a false positive + return resolveSimpleExpression(expr, position); + } + + return result; + } + + /** + * Create a type resolver context that connects the expression resolver + * to ScriptDocument's existing type resolution infrastructure. + */ + private ExpressionNode.TypeResolverContext createExpressionResolverContext(int position) { + return new ExpressionNode.TypeResolverContext() { + @Override + public TypeInfo resolveIdentifier(String name) { + // Check for special keywords first + if ("this".equals(name)) { + return findEnclosingScriptType(position); + } + if ("true".equals(name) || "false".equals(name)) { + return TypeInfo.fromPrimitive("boolean"); + } + if ("null".equals(name)) { + return null; + } + + // Try to resolve as a variable + FieldInfo varInfo = resolveVariable(name, position); + if (varInfo != null) { + return varInfo.getTypeInfo(); + } + + // Try as a class name (for static access) + if (name.length() > 0 && Character.isUpperCase(name.charAt(0))) { + return resolveType(name); + } + + return null; + } + + @Override + public TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName) { + if (targetType == null || !targetType.isResolved()) { + return null; + } + + if (targetType.hasField(memberName)) { + FieldInfo fieldInfo = targetType.getFieldInfo(memberName); + return (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } + + return null; + } + + @Override + public TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes) { + if (targetType == null || !targetType.isResolved()) { + // Try as a script-defined method + if (isScriptMethod(methodName)) { + MethodInfo scriptMethod = getScriptMethodInfo(methodName); + if (scriptMethod != null) { + return scriptMethod.getReturnType(); + } + } + return null; + } + + if (targetType.hasMethod(methodName)) { + MethodInfo methodInfo = targetType.getMethodInfo(methodName); + return (methodInfo != null) ? methodInfo.getReturnType() : null; + } + + return null; + } + + @Override + public TypeInfo resolveArrayAccess(TypeInfo arrayType) { + if (arrayType == null || !arrayType.isResolved()) { + return null; + } + + String typeName = arrayType.getFullName(); + if (typeName.endsWith("[]")) { + String elementTypeName = typeName.substring(0, typeName.length() - 2); + // Try to resolve the element type properly + return resolveType(elementTypeName); + } + + // For List or similar, try to extract the element type + // This is a simplified version - could be enhanced for full generic support + return null; + } + + @Override + public TypeInfo resolveTypeName(String typeName) { + return resolveType(typeName); + } + }; + } + + /** + * Resolve a simple expression without operators. + * This is the fallback path when operator detection was a false positive. + */ + private TypeInfo resolveSimpleExpression(String expr, int position) { + // String literals + if (expr.startsWith("\"") && expr.endsWith("\"")) { + return resolveType("String"); + } + + // Character literals + if (expr.startsWith("'") && expr.endsWith("'")) { + return TypeInfo.fromPrimitive("char"); + } + + // Boolean literals + if (expr.equals("true") || expr.equals("false")) { + return TypeInfo.fromPrimitive("boolean"); + } + + // Null literal + if (expr.equals("null")) { + return null; + } + + // Numeric literals + if (expr.matches("-?\\d*\\.?\\d+[fF]")) { + if (hasExcessivePrecision(expr, 7)) { + return TypeInfo.fromPrimitive("double"); + } + return TypeInfo.fromPrimitive("float"); + } + if (expr.matches("-?\\d*\\.\\d+[dD]?") || expr.matches("-?\\d+\\.[dD]?") || expr.matches("-?\\d+[dD]")) { + if (hasExcessivePrecision(expr, 15)) { + return null; + } + return TypeInfo.fromPrimitive("double"); + } + if (expr.matches("-?\\d+[lL]")) { + return TypeInfo.fromPrimitive("long"); + } + if (expr.matches("-?\\d+")) { + return TypeInfo.fromPrimitive("int"); + } + + // "this" keyword + if (expr.equals("this")) { + return findEnclosingScriptType(position); + } + + // "new Type()" expressions + if (expr.startsWith("new ")) { + Matcher newMatcher = NEW_TYPE_PATTERN.matcher(expr); + if (newMatcher.find()) { + return resolveType(newMatcher.group(1)); + } + } + + // Try as variable or field chain + List segments = parseExpressionChain(expr); + if (segments.isEmpty()) { + return null; + } + + ChainSegment first = segments.get(0); + TypeInfo currentType = null; + + if (first.name.equals("this")) { + currentType = findEnclosingScriptType(position); + } else if (Character.isUpperCase(first.name.charAt(0))) { + currentType = resolveType(first.name); + } else if (!first.isMethodCall) { + FieldInfo varInfo = resolveVariable(first.name, position); + if (varInfo != null) { + currentType = varInfo.getTypeInfo(); + } + } else { + if (isScriptMethod(first.name)) { + MethodInfo scriptMethod = getScriptMethodInfo(first.name); + if (scriptMethod != null) { + currentType = scriptMethod.getReturnType(); + } + } + } + + for (int i = 1; i < segments.size(); i++) { + currentType = resolveChainSegment(currentType, segments.get(i)); + if (currentType == null) { + return null; + } + } + + return currentType; + } + /** * Find the matching opening parenthesis when given a closing paren position. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java new file mode 100644 index 000000000..beb0397c0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java @@ -0,0 +1,232 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class ExpressionNode { + protected final int start; + protected final int end; + + protected ExpressionNode(int start, int end) { + this.start = start; + this.end = end; + } + + public int getStart() { return start; } + public int getEnd() { return end; } + public abstract TypeInfo resolveType(TypeResolverContext resolver); + + public static class IntLiteralNode extends ExpressionNode { + private final String value; + public IntLiteralNode(String value, int start, int end) { super(start, end); this.value = value; } + public String getValue() { return value; } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("int"); } + } + + public static class LongLiteralNode extends ExpressionNode { + public LongLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("long"); } + } + + public static class FloatLiteralNode extends ExpressionNode { + public FloatLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("float"); } + } + + public static class DoubleLiteralNode extends ExpressionNode { + public DoubleLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("double"); } + } + + public static class BooleanLiteralNode extends ExpressionNode { + public BooleanLiteralNode(boolean value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); } + } + + public static class CharLiteralNode extends ExpressionNode { + public CharLiteralNode(String value, int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("char"); } + } + + public static class StringLiteralNode extends ExpressionNode { + private final String value; + public StringLiteralNode(String value, int start, int end) { super(start, end); this.value = value; } + public String getValue() { return value; } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromClass(String.class); } + } + + public static class NullLiteralNode extends ExpressionNode { + public NullLiteralNode(int start, int end) { super(start, end); } + public TypeInfo resolveType(TypeResolverContext resolver) { return null; } + } + + public static class IdentifierNode extends ExpressionNode { + private final String name; + public IdentifierNode(String name, int start, int end) { super(start, end); this.name = name; } + public String getName() { return name; } + public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveIdentifier(name); } + } + + public static class MemberAccessNode extends ExpressionNode { + private final ExpressionNode target; + private final String memberName; + public MemberAccessNode(ExpressionNode target, String memberName, int start, int end) { + super(start, end); this.target = target; this.memberName = memberName; + } + public ExpressionNode getTarget() { return target; } + public String getMemberName() { return memberName; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo targetType = target.resolveType(resolver); + if (targetType == null || !targetType.isResolved()) return null; + return resolver.resolveMemberAccess(targetType, memberName); + } + } + + public static class MethodCallNode extends ExpressionNode { + private final ExpressionNode target; + private final String methodName; + private final List arguments; + public MethodCallNode(ExpressionNode target, String methodName, List arguments, int start, int end) { + super(start, end); this.target = target; this.methodName = methodName; + this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>(); + } + public ExpressionNode getTarget() { return target; } + public String getMethodName() { return methodName; } + public List getArguments() { return Collections.unmodifiableList(arguments); } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo targetType = target != null ? target.resolveType(resolver) : null; + TypeInfo[] argTypes = new TypeInfo[arguments.size()]; + for (int i = 0; i < arguments.size(); i++) argTypes[i] = arguments.get(i).resolveType(resolver); + return resolver.resolveMethodCall(targetType, methodName, argTypes); + } + } + + public static class ArrayAccessNode extends ExpressionNode { + private final ExpressionNode array; + private final ExpressionNode index; + public ArrayAccessNode(ExpressionNode array, ExpressionNode index, int start, int end) { + super(start, end); this.array = array; this.index = index; + } + public ExpressionNode getArray() { return array; } + public ExpressionNode getIndex() { return index; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo arrayType = array.resolveType(resolver); + if (arrayType == null || !arrayType.isResolved()) return null; + return resolver.resolveArrayAccess(arrayType); + } + } + + public static class NewNode extends ExpressionNode { + private final String typeName; + private final List arguments; + public NewNode(String typeName, List arguments, int start, int end) { + super(start, end); this.typeName = typeName; + this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>(); + } + public String getTypeName() { return typeName; } + public List getArguments() { return Collections.unmodifiableList(arguments); } + public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); } + } + + public static class BinaryOpNode extends ExpressionNode { + private final ExpressionNode left; + private final OperatorType operator; + private final ExpressionNode right; + public BinaryOpNode(ExpressionNode left, OperatorType operator, ExpressionNode right, int start, int end) { + super(start, end); this.left = left; this.operator = operator; this.right = right; + } + public ExpressionNode getLeft() { return left; } + public OperatorType getOperator() { return operator; } + public ExpressionNode getRight() { return right; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo leftType = left.resolveType(resolver); + TypeInfo rightType = right.resolveType(resolver); + return TypeRules.resolveBinaryOperatorType(operator, leftType, rightType); + } + } + + public static class UnaryOpNode extends ExpressionNode { + private final OperatorType operator; + private final ExpressionNode operand; + private final boolean prefix; + public UnaryOpNode(OperatorType operator, ExpressionNode operand, boolean prefix, int start, int end) { + super(start, end); this.operator = operator; this.operand = operand; this.prefix = prefix; + } + public OperatorType getOperator() { return operator; } + public ExpressionNode getOperand() { return operand; } + public boolean isPrefix() { return prefix; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo operandType = operand.resolveType(resolver); + return TypeRules.resolveUnaryOperatorType(operator, operandType); + } + } + + public static class TernaryNode extends ExpressionNode { + private final ExpressionNode condition; + private final ExpressionNode thenExpr; + private final ExpressionNode elseExpr; + public TernaryNode(ExpressionNode condition, ExpressionNode thenExpr, ExpressionNode elseExpr, int start, int end) { + super(start, end); this.condition = condition; this.thenExpr = thenExpr; this.elseExpr = elseExpr; + } + public ExpressionNode getCondition() { return condition; } + public ExpressionNode getThenExpr() { return thenExpr; } + public ExpressionNode getElseExpr() { return elseExpr; } + public TypeInfo resolveType(TypeResolverContext resolver) { + TypeInfo thenType = thenExpr.resolveType(resolver); + TypeInfo elseType = elseExpr.resolveType(resolver); + return TypeRules.resolveTernaryType(thenType, elseType); + } + } + + public static class InstanceofNode extends ExpressionNode { + private final ExpressionNode expression; + private final String typeName; + public InstanceofNode(ExpressionNode expression, String typeName, int start, int end) { + super(start, end); this.expression = expression; this.typeName = typeName; + } + public ExpressionNode getExpression() { return expression; } + public String getTypeName() { return typeName; } + public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); } + } + + public static class CastNode extends ExpressionNode { + private final String typeName; + private final ExpressionNode expression; + public CastNode(String typeName, ExpressionNode expression, int start, int end) { + super(start, end); this.typeName = typeName; this.expression = expression; + } + public String getTypeName() { return typeName; } + public ExpressionNode getExpression() { return expression; } + public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); } + } + + public static class AssignmentNode extends ExpressionNode { + private final ExpressionNode target; + private final OperatorType operator; + private final ExpressionNode value; + public AssignmentNode(ExpressionNode target, OperatorType operator, ExpressionNode value, int start, int end) { + super(start, end); this.target = target; this.operator = operator; this.value = value; + } + public ExpressionNode getTarget() { return target; } + public OperatorType getOperator() { return operator; } + public ExpressionNode getValue() { return value; } + public TypeInfo resolveType(TypeResolverContext resolver) { return target.resolveType(resolver); } + } + + public static class ParenthesizedNode extends ExpressionNode { + private final ExpressionNode inner; + public ParenthesizedNode(ExpressionNode inner, int start, int end) { super(start, end); this.inner = inner; } + public ExpressionNode getInner() { return inner; } + public TypeInfo resolveType(TypeResolverContext resolver) { return inner.resolveType(resolver); } + } + + public interface TypeResolverContext { + TypeInfo resolveIdentifier(String name); + TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName); + TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes); + TypeInfo resolveArrayAccess(TypeInfo arrayType); + TypeInfo resolveTypeName(String typeName); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java new file mode 100644 index 000000000..5bb2f9fd7 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java @@ -0,0 +1,331 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import java.util.ArrayList; +import java.util.List; + +public class ExpressionParser { + private final List tokens; + private int pos; + + public ExpressionParser(List tokens) { + this.tokens = tokens; + this.pos = 0; + } + + public ExpressionNode parse() { + if (tokens.isEmpty()) return null; + return parseExpression(0); + } + + private ExpressionToken current() { + if (pos >= tokens.size()) return tokens.get(tokens.size() - 1); + return tokens.get(pos); + } + + private ExpressionToken advance() { + ExpressionToken tok = current(); + if (pos < tokens.size()) pos++; + return tok; + } + + private boolean check(ExpressionToken.TokenKind kind) { return current().getKind() == kind; } + + private boolean match(ExpressionToken.TokenKind kind) { + if (check(kind)) { advance(); return true; } + return false; + } + + private ExpressionNode parseExpression(int minPrecedence) { + ExpressionNode left = parsePrefixExpression(); + if (left == null) return null; + + while (true) { + if (check(ExpressionToken.TokenKind.QUESTION) && minPrecedence <= 2) { + left = parseTernary(left); + continue; + } + + OperatorType op = getCurrentBinaryOperator(); + if (op == null) break; + + int precedence = op.getPrecedence(); + if (precedence < minPrecedence) break; + + advance(); + + int nextMinPrecedence = (op.getAssociativity() == OperatorType.Associativity.LEFT) + ? precedence + 1 : precedence; + + ExpressionNode right = parseExpression(nextMinPrecedence); + if (right == null) break; + + if (op == OperatorType.INSTANCEOF) { + String typeName = extractTypeName(right); + left = new ExpressionNode.InstanceofNode(left, typeName, left.getStart(), right.getEnd()); + } else if (op.isAssignment()) { + left = new ExpressionNode.AssignmentNode(left, op, right, left.getStart(), right.getEnd()); + } else { + left = new ExpressionNode.BinaryOpNode(left, op, right, left.getStart(), right.getEnd()); + } + } + + return parsePostfixExpression(left); + } + + private String extractTypeName(ExpressionNode node) { + if (node instanceof ExpressionNode.IdentifierNode) { + return ((ExpressionNode.IdentifierNode) node).getName(); + } + if (node instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node; + return extractTypeName(ma.getTarget()) + "." + ma.getMemberName(); + } + return "Object"; + } + + private ExpressionNode parseTernary(ExpressionNode condition) { + int start = condition.getStart(); + advance(); + ExpressionNode thenExpr = parseExpression(0); + if (thenExpr == null) return condition; + if (!match(ExpressionToken.TokenKind.COLON)) return condition; + ExpressionNode elseExpr = parseExpression(2); + if (elseExpr == null) return condition; + return new ExpressionNode.TernaryNode(condition, thenExpr, elseExpr, start, elseExpr.getEnd()); + } + + private OperatorType getCurrentBinaryOperator() { + ExpressionToken tok = current(); + if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) { + OperatorType op = tok.getOperatorType(); + if (op != null && (op.isBinary() || op.isAssignment())) return op; + } + if (tok.getKind() == ExpressionToken.TokenKind.INSTANCEOF) return OperatorType.INSTANCEOF; + return null; + } + + private ExpressionNode parsePrefixExpression() { + ExpressionToken tok = current(); + int start = tok.getStart(); + + if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) { + OperatorType op = tok.getOperatorType(); + if (op != null && isUnaryOperator(op)) { + advance(); + ExpressionNode operand = parsePrefixExpression(); + if (operand == null) return null; + OperatorType unaryOp = toUnaryOperator(op); + return new ExpressionNode.UnaryOpNode(unaryOp, operand, true, start, operand.getEnd()); + } + } + + if (tok.getKind() == ExpressionToken.TokenKind.LEFT_PAREN) { + return parseCastOrParenthesized(); + } + + return parsePrimaryExpression(); + } + + private boolean isUnaryOperator(OperatorType op) { + switch (op) { + case ADD: case SUBTRACT: case LOGICAL_NOT: case BITWISE_NOT: + case PRE_INCREMENT: case PRE_DECREMENT: return true; + default: return false; + } + } + + private OperatorType toUnaryOperator(OperatorType op) { + switch (op) { + case ADD: return OperatorType.UNARY_PLUS; + case SUBTRACT: return OperatorType.UNARY_MINUS; + default: return op; + } + } + + private ExpressionNode parseCastOrParenthesized() { + int start = current().getStart(); + advance(); + + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + String possibleType = current().getText(); + int savedPos = pos; + advance(); + + while (check(ExpressionToken.TokenKind.DOT)) { + advance(); + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + possibleType += "." + current().getText(); + advance(); + } + } + + while (check(ExpressionToken.TokenKind.LEFT_BRACKET)) { + advance(); + if (!match(ExpressionToken.TokenKind.RIGHT_BRACKET)) { pos = savedPos; break; } + possibleType += "[]"; + } + + if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + advance(); + if (canStartExpression()) { + ExpressionNode expr = parsePrefixExpression(); + if (expr != null) { + return new ExpressionNode.CastNode(possibleType, expr, start, expr.getEnd()); + } + } + } + pos = savedPos; + } + + ExpressionNode inner = parseExpression(0); + if (inner != null && match(ExpressionToken.TokenKind.RIGHT_PAREN)) { + return new ExpressionNode.ParenthesizedNode(inner, start, current().getStart()); + } + return inner; + } + + private boolean canStartExpression() { + ExpressionToken.TokenKind kind = current().getKind(); + switch (kind) { + case IDENTIFIER: case INT_LITERAL: case LONG_LITERAL: case FLOAT_LITERAL: + case DOUBLE_LITERAL: case BOOLEAN_LITERAL: case CHAR_LITERAL: case STRING_LITERAL: + case NULL_LITERAL: case NEW: case LEFT_PAREN: case OPERATOR: return true; + default: return false; + } + } + + private ExpressionNode parsePrimaryExpression() { + ExpressionToken tok = current(); + int start = tok.getStart(); + + switch (tok.getKind()) { + case INT_LITERAL: advance(); return new ExpressionNode.IntLiteralNode(tok.getText(), start, tok.getEnd()); + case LONG_LITERAL: advance(); return new ExpressionNode.LongLiteralNode(tok.getText(), start, tok.getEnd()); + case FLOAT_LITERAL: advance(); return new ExpressionNode.FloatLiteralNode(tok.getText(), start, tok.getEnd()); + case DOUBLE_LITERAL: advance(); return new ExpressionNode.DoubleLiteralNode(tok.getText(), start, tok.getEnd()); + case BOOLEAN_LITERAL: advance(); return new ExpressionNode.BooleanLiteralNode("true".equals(tok.getText()), start, tok.getEnd()); + case CHAR_LITERAL: advance(); return new ExpressionNode.CharLiteralNode(tok.getText(), start, tok.getEnd()); + case STRING_LITERAL: advance(); return new ExpressionNode.StringLiteralNode(tok.getText(), start, tok.getEnd()); + case NULL_LITERAL: advance(); return new ExpressionNode.NullLiteralNode(start, tok.getEnd()); + case NEW: return parseNewExpression(); + case IDENTIFIER: return parseIdentifierOrMethodCall(); + case LEFT_PAREN: return parseCastOrParenthesized(); + default: return null; + } + } + + private ExpressionNode parseNewExpression() { + int start = current().getStart(); + advance(); + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) return null; + + StringBuilder typeName = new StringBuilder(current().getText()); + advance(); + + while (check(ExpressionToken.TokenKind.DOT)) { + advance(); + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + typeName.append(".").append(current().getText()); + advance(); + } + } + + List args = new ArrayList<>(); + if (match(ExpressionToken.TokenKind.LEFT_PAREN)) { + args = parseArgumentList(); + match(ExpressionToken.TokenKind.RIGHT_PAREN); + } + + return new ExpressionNode.NewNode(typeName.toString(), args, start, current().getStart()); + } + + private ExpressionNode parseIdentifierOrMethodCall() { + int start = current().getStart(); + String name = current().getText(); + advance(); + + ExpressionNode result = new ExpressionNode.IdentifierNode(name, start, current().getStart()); + return parseAccessChain(result); + } + + private ExpressionNode parseAccessChain(ExpressionNode base) { + while (true) { + if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { + advance(); + List args = parseArgumentList(); + int end = current().getStart(); + match(ExpressionToken.TokenKind.RIGHT_PAREN); + + String methodName; + ExpressionNode target; + + if (base instanceof ExpressionNode.IdentifierNode) { + methodName = ((ExpressionNode.IdentifierNode) base).getName(); + target = null; + } else if (base instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) base; + methodName = ma.getMemberName(); + target = ma.getTarget(); + } else { + methodName = "apply"; + target = base; + } + + base = new ExpressionNode.MethodCallNode(target, methodName, args, base.getStart(), end); + } else if (check(ExpressionToken.TokenKind.DOT)) { + advance(); + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) break; + String memberName = current().getText(); + int end = current().getEnd(); + advance(); + base = new ExpressionNode.MemberAccessNode(base, memberName, base.getStart(), end); + } else if (check(ExpressionToken.TokenKind.LEFT_BRACKET)) { + advance(); + ExpressionNode index = parseExpression(0); + int end = current().getStart(); + match(ExpressionToken.TokenKind.RIGHT_BRACKET); + if (index != null) { + base = new ExpressionNode.ArrayAccessNode(base, index, base.getStart(), end); + } + } else { + break; + } + } + return base; + } + + private List parseArgumentList() { + List args = new ArrayList<>(); + if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) return args; + + ExpressionNode first = parseExpression(0); + if (first != null) args.add(first); + + while (match(ExpressionToken.TokenKind.COMMA)) { + ExpressionNode arg = parseExpression(0); + if (arg != null) args.add(arg); + } + return args; + } + + private ExpressionNode parsePostfixExpression(ExpressionNode expr) { + if (expr == null) return null; + + while (true) { + ExpressionToken tok = current(); + if (tok.getKind() != ExpressionToken.TokenKind.OPERATOR) break; + + OperatorType op = tok.getOperatorType(); + if (op == OperatorType.PRE_INCREMENT) { + advance(); + expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_INCREMENT, expr, false, expr.getStart(), tok.getEnd()); + } else if (op == OperatorType.PRE_DECREMENT) { + advance(); + expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_DECREMENT, expr, false, expr.getStart(), tok.getEnd()); + } else { + break; + } + } + return expr; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java new file mode 100644 index 000000000..655adea9a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java @@ -0,0 +1,60 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +public class ExpressionToken { + + public enum TokenKind { + INT_LITERAL, LONG_LITERAL, FLOAT_LITERAL, DOUBLE_LITERAL, + BOOLEAN_LITERAL, CHAR_LITERAL, STRING_LITERAL, NULL_LITERAL, + IDENTIFIER, NEW, INSTANCEOF, + OPERATOR, + LEFT_PAREN, RIGHT_PAREN, LEFT_BRACKET, RIGHT_BRACKET, + DOT, COMMA, QUESTION, COLON, SEMICOLON, + EOF + } + + private final TokenKind kind; + private final String text; + private final int start; + private final int end; + private final OperatorType operatorType; + + public ExpressionToken(TokenKind kind, String text, int start, int end) { + this(kind, text, start, end, null); + } + + public ExpressionToken(TokenKind kind, String text, int start, int end, OperatorType operatorType) { + this.kind = kind; + this.text = text; + this.start = start; + this.end = end; + this.operatorType = operatorType; + } + + public TokenKind getKind() { return kind; } + public String getText() { return text; } + public int getStart() { return start; } + public int getEnd() { return end; } + public OperatorType getOperatorType() { return operatorType; } + + public static ExpressionToken operator(String symbol, int start, int end) { + OperatorType op = OperatorType.fromBinarySymbol(symbol); + if (op == null) op = OperatorType.fromSymbol(symbol); + return new ExpressionToken(TokenKind.OPERATOR, symbol, start, end, op); + } + + public static ExpressionToken identifier(String name, int start, int end) { + if ("true".equals(name) || "false".equals(name)) { + return new ExpressionToken(TokenKind.BOOLEAN_LITERAL, name, start, end); + } + if ("null".equals(name)) { + return new ExpressionToken(TokenKind.NULL_LITERAL, name, start, end); + } + if ("new".equals(name)) { + return new ExpressionToken(TokenKind.NEW, name, start, end); + } + if ("instanceof".equals(name)) { + return new ExpressionToken(TokenKind.INSTANCEOF, name, start, end); + } + return new ExpressionToken(TokenKind.IDENTIFIER, name, start, end); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java new file mode 100644 index 000000000..25ee10ea5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java @@ -0,0 +1,152 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import java.util.ArrayList; +import java.util.List; + +public class ExpressionTokenizer { + + public static List tokenize(String expr) { + List tokens = new ArrayList<>(); + int pos = 0; + int len = expr.length(); + + while (pos < len) { + char c = expr.charAt(pos); + + if (Character.isWhitespace(c)) { pos++; continue; } + + if (c == '"') { + int start = pos++; + while (pos < len && expr.charAt(pos) != '"') { + if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++; + pos++; + } + if (pos < len) pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.STRING_LITERAL, expr.substring(start, pos), start, pos)); + continue; + } + + if (c == '\'') { + int start = pos++; + while (pos < len && expr.charAt(pos) != '\'') { + if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++; + pos++; + } + if (pos < len) pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.CHAR_LITERAL, expr.substring(start, pos), start, pos)); + continue; + } + + if (Character.isDigit(c) || (c == '.' && pos + 1 < len && Character.isDigit(expr.charAt(pos + 1)))) { + int start = pos; + if (c == '0' && pos + 1 < len) { + char next = expr.charAt(pos + 1); + if (next == 'x' || next == 'X') { + pos += 2; + while (pos < len && isHexDigit(expr.charAt(pos))) pos++; + if (pos < len && (expr.charAt(pos) == 'L' || expr.charAt(pos) == 'l')) { + pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LONG_LITERAL, expr.substring(start, pos), start, pos)); + } else { + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos)); + } + continue; + } + if (next == 'b' || next == 'B') { + pos += 2; + while (pos < len && (expr.charAt(pos) == '0' || expr.charAt(pos) == '1')) pos++; + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos)); + continue; + } + } + + while (pos < len && Character.isDigit(expr.charAt(pos))) pos++; + boolean hasDecimal = false; + if (pos < len && expr.charAt(pos) == '.') { + if (pos + 1 < len && Character.isDigit(expr.charAt(pos + 1))) { + hasDecimal = true; + pos++; + while (pos < len && Character.isDigit(expr.charAt(pos))) pos++; + } + } + if (pos < len && (expr.charAt(pos) == 'e' || expr.charAt(pos) == 'E')) { + pos++; + if (pos < len && (expr.charAt(pos) == '+' || expr.charAt(pos) == '-')) pos++; + while (pos < len && Character.isDigit(expr.charAt(pos))) pos++; + hasDecimal = true; + } + + ExpressionToken.TokenKind kind = ExpressionToken.TokenKind.INT_LITERAL; + if (pos < len) { + char suffix = expr.charAt(pos); + if (suffix == 'f' || suffix == 'F') { kind = ExpressionToken.TokenKind.FLOAT_LITERAL; pos++; } + else if (suffix == 'd' || suffix == 'D') { kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; pos++; } + else if (suffix == 'L' || suffix == 'l') { kind = ExpressionToken.TokenKind.LONG_LITERAL; pos++; } + else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; + } else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; + + tokens.add(new ExpressionToken(kind, expr.substring(start, pos), start, pos)); + continue; + } + + if (Character.isJavaIdentifierStart(c)) { + int start = pos; + while (pos < len && Character.isJavaIdentifierPart(expr.charAt(pos))) pos++; + tokens.add(ExpressionToken.identifier(expr.substring(start, pos), start, pos)); + continue; + } + + int start = pos; + String op = null; + if (pos + 3 <= len) { + String s3 = expr.substring(pos, pos + 3); + if (">>>".equals(s3) || "<<=".equals(s3) || ">>=".equals(s3)) op = s3; + } + if (op == null && pos + 4 <= len) { + String s4 = expr.substring(pos, pos + 4); + if (">>>=".equals(s4)) op = s4; + } + if (op == null && pos + 2 <= len) { + String s2 = expr.substring(pos, pos + 2); + if ("++".equals(s2) || "--".equals(s2) || "+=".equals(s2) || "-=".equals(s2) || + "*=".equals(s2) || "/=".equals(s2) || "%=".equals(s2) || "&=".equals(s2) || + "|=".equals(s2) || "^=".equals(s2) || "==".equals(s2) || "!=".equals(s2) || + "<=".equals(s2) || ">=".equals(s2) || "&&".equals(s2) || "||".equals(s2) || + "<<".equals(s2) || ">>".equals(s2)) op = s2; + } + if (op == null) { + switch (c) { + case '+': case '-': case '*': case '/': case '%': case '&': case '|': + case '^': case '~': case '!': case '<': case '>': case '=': + op = String.valueOf(c); break; + } + } + + if (op != null) { + pos += op.length(); + tokens.add(ExpressionToken.operator(op, start, pos)); + continue; + } + + switch (c) { + case '(': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_PAREN, "(", pos, pos + 1)); break; + case ')': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_PAREN, ")", pos, pos + 1)); break; + case '[': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACKET, "[", pos, pos + 1)); break; + case ']': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACKET, "]", pos, pos + 1)); break; + case '.': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.DOT, ".", pos, pos + 1)); break; + case ',': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COMMA, ",", pos, pos + 1)); break; + case '?': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.QUESTION, "?", pos, pos + 1)); break; + case ':': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COLON, ":", pos, pos + 1)); break; + case ';': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.SEMICOLON, ";", pos, pos + 1)); break; + } + pos++; + } + + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.EOF, "", len, len)); + return tokens; + } + + private static boolean isHexDigit(char c) { + return Character.isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java new file mode 100644 index 000000000..c07ffefcf --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -0,0 +1,167 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.util.List; + +public class ExpressionTypeResolver { + private final ExpressionNode.TypeResolverContext context; + + public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) { + this.context = context; + } + + public TypeInfo resolve(String expression) { + if (expression == null || expression.trim().isEmpty()) return null; + + try { + List tokens = ExpressionTokenizer.tokenize(expression); + if (tokens.isEmpty()) return null; + + ExpressionParser parser = new ExpressionParser(tokens); + ExpressionNode ast = parser.parse(); + if (ast == null) return null; + + return resolveNodeType(ast); + } catch (Exception e) { + return null; + } + } + + public static boolean containsOperators(String expression) { + if (expression == null) return false; + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + switch (c) { + case '+': case '-': case '*': case '/': case '%': + case '&': case '|': case '^': case '~': + case '<': case '>': case '=': case '!': + case '?': case ':': return true; + } + } + return false; + } + + private TypeInfo resolveNodeType(ExpressionNode node) { + if (node == null) return null; + + if (node instanceof ExpressionNode.IntLiteralNode) return TypeInfo.fromPrimitive("int"); + if (node instanceof ExpressionNode.LongLiteralNode) return TypeInfo.fromPrimitive("long"); + if (node instanceof ExpressionNode.FloatLiteralNode) return TypeInfo.fromPrimitive("float"); + if (node instanceof ExpressionNode.DoubleLiteralNode) return TypeInfo.fromPrimitive("double"); + if (node instanceof ExpressionNode.BooleanLiteralNode) return TypeInfo.fromPrimitive("boolean"); + if (node instanceof ExpressionNode.CharLiteralNode) return TypeInfo.fromPrimitive("char"); + if (node instanceof ExpressionNode.StringLiteralNode) return TypeInfo.fromClass(String.class); + if (node instanceof ExpressionNode.NullLiteralNode) return null; + + if (node instanceof ExpressionNode.IdentifierNode) { + return context.resolveIdentifier(((ExpressionNode.IdentifierNode) node).getName()); + } + + if (node instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node; + TypeInfo targetType = resolveNodeType(ma.getTarget()); + if (targetType == null || !targetType.isResolved()) return null; + return context.resolveMemberAccess(targetType, ma.getMemberName()); + } + + if (node instanceof ExpressionNode.MethodCallNode) { + ExpressionNode.MethodCallNode mc = (ExpressionNode.MethodCallNode) node; + TypeInfo targetType = mc.getTarget() == null ? context.resolveIdentifier("this") : resolveNodeType(mc.getTarget()); + if (targetType == null || !targetType.isResolved()) return null; + TypeInfo[] argTypes = new TypeInfo[mc.getArguments().size()]; + for (int i = 0; i < argTypes.length; i++) argTypes[i] = resolveNodeType(mc.getArguments().get(i)); + return context.resolveMethodCall(targetType, mc.getMethodName(), argTypes); + } + + if (node instanceof ExpressionNode.ArrayAccessNode) { + ExpressionNode.ArrayAccessNode aa = (ExpressionNode.ArrayAccessNode) node; + TypeInfo arrayType = resolveNodeType(aa.getArray()); + if (arrayType == null || !arrayType.isResolved()) return null; + String typeName = arrayType.getFullName(); + if (typeName.endsWith("[]")) { + String elementTypeName = typeName.substring(0, typeName.length() - 2); + return context.resolveTypeName(elementTypeName); + } + return context.resolveArrayAccess(arrayType); + } + + if (node instanceof ExpressionNode.NewNode) { + return context.resolveTypeName(((ExpressionNode.NewNode) node).getTypeName()); + } + + if (node instanceof ExpressionNode.BinaryOpNode) { + ExpressionNode.BinaryOpNode bin = (ExpressionNode.BinaryOpNode) node; + TypeInfo leftType = resolveNodeType(bin.getLeft()); + TypeInfo rightType = resolveNodeType(bin.getRight()); + return TypeRules.resolveBinaryOperatorType(bin.getOperator(), leftType, rightType); + } + + if (node instanceof ExpressionNode.UnaryOpNode) { + ExpressionNode.UnaryOpNode un = (ExpressionNode.UnaryOpNode) node; + TypeInfo operandType = resolveNodeType(un.getOperand()); + return TypeRules.resolveUnaryOperatorType(un.getOperator(), operandType); + } + + if (node instanceof ExpressionNode.TernaryNode) { + ExpressionNode.TernaryNode tern = (ExpressionNode.TernaryNode) node; + TypeInfo thenType = resolveNodeType(tern.getThenExpr()); + TypeInfo elseType = resolveNodeType(tern.getElseExpr()); + return TypeRules.resolveTernaryType(thenType, elseType); + } + + if (node instanceof ExpressionNode.CastNode) { + return context.resolveTypeName(((ExpressionNode.CastNode) node).getTypeName()); + } + + if (node instanceof ExpressionNode.InstanceofNode) { + return TypeInfo.fromPrimitive("boolean"); + } + + if (node instanceof ExpressionNode.AssignmentNode) { + return resolveNodeType(((ExpressionNode.AssignmentNode) node).getTarget()); + } + + if (node instanceof ExpressionNode.ParenthesizedNode) { + return resolveNodeType(((ExpressionNode.ParenthesizedNode) node).getInner()); + } + + return null; + } + + public static ExpressionNode.TypeResolverContext createBasicContext() { + return new ExpressionNode.TypeResolverContext() { + public TypeInfo resolveIdentifier(String name) { + if ("true".equals(name) || "false".equals(name)) return TypeInfo.fromPrimitive("boolean"); + if ("null".equals(name)) return null; + return null; + } + public TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName) { return null; } + public TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes) { return null; } + public TypeInfo resolveArrayAccess(TypeInfo arrayType) { + String typeName = arrayType.getFullName(); + if (typeName.endsWith("[]")) { + // For primitive arrays, we need to map back to primitive TypeInfo + String elementType = typeName.substring(0, typeName.length() - 2); + switch (elementType) { + case "int": case "long": case "float": case "double": + case "byte": case "short": case "char": case "boolean": + return TypeInfo.fromPrimitive(elementType); + default: + return TypeInfo.unresolved(elementType, elementType); + } + } + return null; + } + public TypeInfo resolveTypeName(String typeName) { + // Handle primitives + switch (typeName) { + case "int": case "long": case "float": case "double": + case "byte": case "short": case "char": case "boolean": case "void": + return TypeInfo.fromPrimitive(typeName); + default: + return TypeInfo.unresolved(typeName, typeName); + } + } + }; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java new file mode 100644 index 000000000..2d4bf32dc --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/OperatorType.java @@ -0,0 +1,147 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +public enum OperatorType { + + ADD("+", 11, Associativity.LEFT, Category.ARITHMETIC), + SUBTRACT("-", 11, Associativity.LEFT, Category.ARITHMETIC), + MULTIPLY("*", 12, Associativity.LEFT, Category.ARITHMETIC), + DIVIDE("/", 12, Associativity.LEFT, Category.ARITHMETIC), + MODULO("%", 12, Associativity.LEFT, Category.ARITHMETIC), + + + ASSIGN("=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + ADD_ASSIGN("+=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + SUBTRACT_ASSIGN("-=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + MULTIPLY_ASSIGN("*=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + DIVIDE_ASSIGN("/=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + MODULO_ASSIGN("%=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + AND_ASSIGN("&=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + OR_ASSIGN("|=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + XOR_ASSIGN("^=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + LEFT_SHIFT_ASSIGN("<<=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + RIGHT_SHIFT_ASSIGN(">>=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + UNSIGNED_RIGHT_SHIFT_ASSIGN(">>>=", 1, Associativity.RIGHT, Category.ASSIGNMENT), + + TERNARY_QUESTION("?", 2, Associativity.RIGHT, Category.TERNARY), + TERNARY_COLON(":", 2, Associativity.RIGHT, Category.TERNARY), + + LOGICAL_OR("||", 3, Associativity.LEFT, Category.LOGICAL), + LOGICAL_AND("&&", 4, Associativity.LEFT, Category.LOGICAL), + + BITWISE_OR("|", 5, Associativity.LEFT, Category.BITWISE), + BITWISE_XOR("^", 6, Associativity.LEFT, Category.BITWISE), + BITWISE_AND("&", 7, Associativity.LEFT, Category.BITWISE), + + EQUALS("==", 8, Associativity.LEFT, Category.RELATIONAL), + NOT_EQUALS("!=", 8, Associativity.LEFT, Category.RELATIONAL), + LESS_THAN("<", 9, Associativity.LEFT, Category.RELATIONAL), + GREATER_THAN(">", 9, Associativity.LEFT, Category.RELATIONAL), + LESS_THAN_OR_EQUAL("<=", 9, Associativity.LEFT, Category.RELATIONAL), + GREATER_THAN_OR_EQUAL(">=", 9, Associativity.LEFT, Category.RELATIONAL), + + LEFT_SHIFT("<<", 10, Associativity.LEFT, Category.BITWISE), + RIGHT_SHIFT(">>", 10, Associativity.LEFT, Category.BITWISE), + UNSIGNED_RIGHT_SHIFT(">>>", 10, Associativity.LEFT, Category.BITWISE), + + UNARY_PLUS("+", 14, Associativity.RIGHT, Category.UNARY), + UNARY_MINUS("-", 14, Associativity.RIGHT, Category.UNARY), + LOGICAL_NOT("!", 14, Associativity.RIGHT, Category.UNARY), + BITWISE_NOT("~", 14, Associativity.RIGHT, Category.UNARY), + PRE_INCREMENT("++", 14, Associativity.RIGHT, Category.UNARY), + PRE_DECREMENT("--", 14, Associativity.RIGHT, Category.UNARY), + POST_INCREMENT("++", 15, Associativity.LEFT, Category.UNARY_POSTFIX), + POST_DECREMENT("--", 15, Associativity.LEFT, Category.UNARY_POSTFIX), + + MEMBER_ACCESS(".", 16, Associativity.LEFT, Category.ACCESS), + ARRAY_ACCESS("[]", 16, Associativity.LEFT, Category.ACCESS), + METHOD_CALL("()", 16, Associativity.LEFT, Category.ACCESS), + + CAST("(type)", 14, Associativity.RIGHT, Category.CAST), + INSTANCEOF("instanceof", 9, Associativity.LEFT, Category.INSTANCEOF); + + + private final String symbol; + private final int precedence; + private final Associativity associativity; + private final Category category; + + OperatorType(String symbol, int precedence, Associativity associativity, Category category) { + this.symbol = symbol; + this.precedence = precedence; + this.associativity = associativity; + this.category = category; + } + + public String getSymbol() { return symbol; } + public int getPrecedence() { return precedence; } + public Associativity getAssociativity() { return associativity; } + public Category getCategory() { return category; } + + public boolean isBinary() { + return category == Category.ARITHMETIC || category == Category.RELATIONAL || + category == Category.LOGICAL || category == Category.BITWISE; + } + + public boolean isUnary() { + return category == Category.UNARY || category == Category.UNARY_POSTFIX; + } + + public boolean isAssignment() { + return category == Category.ASSIGNMENT; + } + + public boolean isComparison() { + return category == Category.RELATIONAL; + } + + public enum Associativity { LEFT, RIGHT } + + public enum Category { + ARITHMETIC, UNARY, UNARY_POSTFIX, RELATIONAL, LOGICAL, BITWISE, + TERNARY, INSTANCEOF, ASSIGNMENT, CAST, ACCESS + } + + public static OperatorType fromSymbol(String symbol) { + for (OperatorType op : values()) { + if (op.symbol.equals(symbol)) return op; + } + return null; + } + + public static OperatorType fromBinarySymbol(String symbol) { + switch (symbol) { + case "+": return ADD; + case "-": return SUBTRACT; + case "*": return MULTIPLY; + case "/": return DIVIDE; + case "%": return MODULO; + case "==": return EQUALS; + case "!=": return NOT_EQUALS; + case "<": return LESS_THAN; + case ">": return GREATER_THAN; + case "<=": return LESS_THAN_OR_EQUAL; + case ">=": return GREATER_THAN_OR_EQUAL; + case "&&": return LOGICAL_AND; + case "||": return LOGICAL_OR; + case "&": return BITWISE_AND; + case "|": return BITWISE_OR; + case "^": return BITWISE_XOR; + case "<<": return LEFT_SHIFT; + case ">>": return RIGHT_SHIFT; + case ">>>": return UNSIGNED_RIGHT_SHIFT; + case "=": return ASSIGN; + case "+=": return ADD_ASSIGN; + case "-=": return SUBTRACT_ASSIGN; + case "*=": return MULTIPLY_ASSIGN; + case "/=": return DIVIDE_ASSIGN; + case "%=": return MODULO_ASSIGN; + case "&=": return AND_ASSIGN; + case "|=": return OR_ASSIGN; + case "^=": return XOR_ASSIGN; + case "<<=": return LEFT_SHIFT_ASSIGN; + case ">>=": return RIGHT_SHIFT_ASSIGN; + case ">>>=": return UNSIGNED_RIGHT_SHIFT_ASSIGN; + default: return null; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java new file mode 100644 index 000000000..8b3fa8b17 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java @@ -0,0 +1,157 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +public class TypeRules { + + public static boolean isNumeric(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + return "int".equals(name) || "long".equals(name) || "float".equals(name) || + "double".equals(name) || "byte".equals(name) || "short".equals(name) || "char".equals(name); + } + + public static boolean isIntegral(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + return "int".equals(name) || "long".equals(name) || "byte".equals(name) || + "short".equals(name) || "char".equals(name); + } + + public static boolean isFloatingPoint(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + return "float".equals(name) || "double".equals(name); + } + + public static boolean isBoolean(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + return "boolean".equals(type.getSimpleName()); + } + + public static boolean isString(TypeInfo type) { + if (type == null || !type.isResolved()) return false; + String name = type.getSimpleName(); + String fullName = type.getFullName(); + return "String".equals(name) || "java.lang.String".equals(fullName); + } + + public static TypeInfo binaryNumericPromotion(TypeInfo left, TypeInfo right) { + if (left == null || right == null || !left.isResolved() || !right.isResolved()) { + return null; + } + String l = left.getSimpleName(); + String r = right.getSimpleName(); + if ("double".equals(l) || "double".equals(r)) return TypeInfo.fromPrimitive("double"); + if ("float".equals(l) || "float".equals(r)) return TypeInfo.fromPrimitive("float"); + if ("long".equals(l) || "long".equals(r)) return TypeInfo.fromPrimitive("long"); + return TypeInfo.fromPrimitive("int"); + } + + public static TypeInfo unaryNumericPromotion(TypeInfo type) { + if (type == null || !type.isResolved()) return null; + String name = type.getSimpleName(); + if ("double".equals(name) || "float".equals(name) || "long".equals(name)) return type; + if ("byte".equals(name) || "short".equals(name) || "char".equals(name) || "int".equals(name)) { + return TypeInfo.fromPrimitive("int"); + } + return null; + } + + public static TypeInfo resolveBinaryOperatorType(OperatorType op, TypeInfo left, TypeInfo right) { + if (op == null) return null; + + switch (op.getCategory()) { + case ARITHMETIC: + if (op == OperatorType.ADD && (isString(left) || isString(right))) { + return TypeInfo.fromClass(String.class); + } + if (isNumeric(left) && isNumeric(right)) { + return binaryNumericPromotion(left, right); + } + return null; + + case RELATIONAL: + if (op == OperatorType.EQUALS || op == OperatorType.NOT_EQUALS) { + return TypeInfo.fromPrimitive("boolean"); + } + if (isNumeric(left) && isNumeric(right)) { + return TypeInfo.fromPrimitive("boolean"); + } + return null; + + case LOGICAL: + if (isBoolean(left) && isBoolean(right)) { + return TypeInfo.fromPrimitive("boolean"); + } + return null; + + case BITWISE: + if (op == OperatorType.LEFT_SHIFT || op == OperatorType.RIGHT_SHIFT || + op == OperatorType.UNSIGNED_RIGHT_SHIFT) { + if (isIntegral(left)) { + return unaryNumericPromotion(left); + } + } + if (isIntegral(left) && isIntegral(right)) { + return binaryNumericPromotion(left, right); + } + if (isBoolean(left) && isBoolean(right)) { + return TypeInfo.fromPrimitive("boolean"); + } + return null; + + case ASSIGNMENT: + return left; + + default: + return null; + } + } + + public static TypeInfo resolveUnaryOperatorType(OperatorType op, TypeInfo operand) { + if (op == null || operand == null || !operand.isResolved()) { + return null; + } + + switch (op) { + case UNARY_PLUS: + case UNARY_MINUS: + if (isNumeric(operand)) return unaryNumericPromotion(operand); + return null; + + case BITWISE_NOT: + if (isIntegral(operand)) return unaryNumericPromotion(operand); + return null; + + case LOGICAL_NOT: + if (isBoolean(operand)) return TypeInfo.fromPrimitive("boolean"); + return null; + + case PRE_INCREMENT: + case PRE_DECREMENT: + case POST_INCREMENT: + case POST_DECREMENT: + if (isNumeric(operand)) return operand; + return null; + + default: + return null; + } + } + + public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) { + if (thenType == null && elseType == null) return null; + if (thenType == null) return elseType; + if (elseType == null) return thenType; + if (!thenType.isResolved()) return elseType.isResolved() ? elseType : null; + if (!elseType.isResolved()) return thenType; + + if (thenType.equals(elseType)) return thenType; + if (isNumeric(thenType) && isNumeric(elseType)) { + return binaryNumericPromotion(thenType, elseType); + } + + return thenType; + } +} From 574c893f8de34d52132ccba47bf589ae52aaa49b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 22:37:37 +0200 Subject: [PATCH 085/337] oops --- .../client/gui/util/script/interpreter/token/Token.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index efd917dc7..be4bbc044 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -137,9 +137,9 @@ public static Token undefined(String text, int start, int end) { public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } - void setParentLine(ScriptLine line) { this.parentLine = line; } - void setPrev(Token prev) { this.prev = prev; } - void setNext(Token next) { this.next = next; } + public void setParentLine(ScriptLine line) { this.parentLine = line; } + public void setPrev(Token prev) { this.prev = prev; } + public void setNext(Token next) { this.next = next; } // ==================== NAVIGATION ==================== From 3a2bcd228f0780e49a1afd76763c6e42d6cd0c6c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 28 Dec 2025 23:03:52 +0200 Subject: [PATCH 086/337] Fixed members of script types being unable to reference their type on creation --- .../client/gui/util/script/interpreter/ScriptDocument.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 7554c1000..5a135bf6a 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 @@ -382,11 +382,12 @@ private void parseScriptTypes() { ScriptTypeInfo scriptType = ScriptTypeInfo.create( typeName, kind, m.start(), bodyStart, bodyEnd, modifiers); - - // Parse fields and methods inside this type + + scriptTypes.put(typeName, scriptType); + + // Parse fields and methods inside this type AFTER adding the scriptType globally, so its members can reference it parseScriptTypeMembers(scriptType); - scriptTypes.put(typeName, scriptType); } } From 595efb529c79f61b44f49296b88c3e3bf1510315 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 00:01:54 +0200 Subject: [PATCH 087/337] Unified isTypeCompatible across classes --- .../interpreter/field/AssignmentInfo.java | 107 +----------------- .../interpreter/field/FieldAccessInfo.java | 31 +---- .../interpreter/method/MethodCallInfo.java | 76 +------------ .../script/interpreter/type/TypeChecker.java | 102 ++++++++++++++++- 4 files changed, 105 insertions(+), 211 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java index 1a1700939..c1eaeef94 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/AssignmentInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter.field; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.lang.reflect.Field; @@ -134,7 +135,7 @@ public void validate() { // Check type compatibility if (targetType != null && sourceType != null) { - if (!isTypeCompatible(sourceType, targetType)) { + if (!TypeChecker.isTypeCompatible(targetType, sourceType)) { this.requiredType = targetType.getSimpleName(); this.providedType = sourceType.getSimpleName(); setError(ErrorType.TYPE_MISMATCH, buildTypeMismatchMessage()); @@ -149,110 +150,6 @@ private String buildTypeMismatchMessage() { return "Provided type: " + providedType + "\nRequired: " + requiredType; } - /** - * Check if sourceType can be assigned to targetType. - */ - private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { - if (sourceType == null || targetType == null) { - return true; // Can't validate, assume compatible - } - - // Same type name - if (sourceType.getFullName().equals(targetType.getFullName())) { - return true; - } - - // Same simple name (for primitives and common types) - if (sourceType.getSimpleName().equals(targetType.getSimpleName())) { - return true; - } - - // Check if sourceType is a subtype of targetType via reflection - if (sourceType.isResolved() && targetType.isResolved()) { - Class sourceClass = sourceType.getJavaClass(); - Class targetClass = targetType.getJavaClass(); - - if (sourceClass != null && targetClass != null) { - // Direct assignability - if (targetClass.isAssignableFrom(sourceClass)) { - return true; - } - - // Primitive widening conversions - if (isPrimitiveWidening(sourceClass, targetClass)) { - return true; - } - - // Boxing/unboxing - if (isBoxingCompatible(sourceClass, targetClass)) { - return true; - } - } - } - - return false; - } - - /** - * Check for primitive widening conversions. - * byte -> short -> int -> long -> float -> double - * char -> int -> long -> float -> double - */ - private boolean isPrimitiveWidening(Class from, Class to) { - if (!from.isPrimitive() || !to.isPrimitive()) { - return false; - } - - if (from == byte.class) { - return to == short.class || to == int.class || to == long.class || - to == float.class || to == double.class; - } - if (from == short.class || from == char.class) { - return to == int.class || to == long.class || to == float.class || to == double.class; - } - if (from == int.class) { - return to == long.class || to == float.class || to == double.class; - } - if (from == long.class) { - return to == float.class || to == double.class; - } - if (from == float.class) { - return to == double.class; - } - return false; - } - - /** - * Check for boxing/unboxing compatibility. - */ - private boolean isBoxingCompatible(Class from, Class to) { - if (from.isPrimitive()) { - Class wrapper = getWrapperClass(from); - if (wrapper != null && to.isAssignableFrom(wrapper)) { - return true; - } - } - if (to.isPrimitive()) { - Class wrapper = getWrapperClass(to); - if (wrapper != null && wrapper.isAssignableFrom(from)) { - return true; - } - } - return false; - } - - private Class getWrapperClass(Class primitive) { - if (primitive == boolean.class) return Boolean.class; - if (primitive == byte.class) return Byte.class; - if (primitive == char.class) return Character.class; - if (primitive == short.class) return Short.class; - if (primitive == int.class) return Integer.class; - if (primitive == long.class) return Long.class; - if (primitive == float.class) return Float.class; - if (primitive == double.class) return Double.class; - return null; - } - private void setError(ErrorType type, String message) { this.errorType = type; this.errorMessage = message; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java index 740ae1f46..3e0ae760d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter.field; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; /** @@ -94,7 +95,7 @@ public void validate() { // Check return type compatibility with expected type (e.g., assignment LHS) if (expectedType != null && resolvedField != null) { TypeInfo fieldType = resolvedField.getDeclaredType(); - if (fieldType != null && !isTypeCompatible(fieldType, expectedType)) { + if (fieldType != null && !TypeChecker.isTypeCompatible(expectedType, fieldType)) { //extra space is necessary for alignment //setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+ //"\nRequired: " + expectedType.getSimpleName()); @@ -102,34 +103,6 @@ public void validate() { } } - /** - * Check if sourceType can be assigned to targetType. - */ - private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { - if (sourceType == null || targetType == null) { - return true; // Can't validate, assume compatible - } - - // Same type - if (sourceType.getFullName().equals(targetType.getFullName())) { - return true; - } - - // Check if sourceType is a subtype of targetType - if (sourceType.isResolved() && targetType.isResolved()) { - Class sourceClass = sourceType.getJavaClass(); - Class targetClass = targetType.getJavaClass(); - - if (sourceClass != null && targetClass != null) { - return targetClass.isAssignableFrom(sourceClass); - } - } - - // Primitive widening/boxing conversions would go here - // For now, just check direct equality - return false; - } - private void setError(ErrorType type, String message) { this.errorType = type; this.errorMessage = message; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index 21aac4e20..64438c8f9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -2,6 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.ArrayList; @@ -348,7 +349,7 @@ public void validate() { TypeInfo argType = arg.getResolvedType(); TypeInfo paramType = para.getDeclaredType(); if (argType != null && paramType != null) { - if (!isTypeCompatible(argType, paramType)) { + if (!TypeChecker.isTypeCompatible(paramType, argType)) { setArgTypeError(i, "Expected " + paramType.getSimpleName() + " but got " + argType.getSimpleName()); } @@ -362,84 +363,13 @@ public void validate() { // Check return type compatibility with expected type (e.g., assignment LHS) if (expectedType != null && resolvedMethod != null) { TypeInfo returnType = resolvedMethod.getReturnType(); - if (returnType != null && !isTypeCompatible(returnType, expectedType)) { + if (returnType != null && !TypeChecker.isTypeCompatible(expectedType, returnType)) { // setError(ErrorType.RETURN_TYPE_MISMATCH, // "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName()); } } } - /** - * Check if sourceType can be assigned to targetType. - * Handles primitive widening, inheritance, etc. - */ - private boolean isTypeCompatible(TypeInfo sourceType, TypeInfo targetType) { - if (sourceType == null || targetType == null) { - return true; // Can't verify, assume compatible - } - - if (sourceType.equals(targetType)) { - return true; - } - - Class sourceClass = sourceType.getJavaClass(); - Class targetClass = targetType.getJavaClass(); - - if (sourceClass == null || targetClass == null) { - return true; // Can't verify, assume compatible - } - - // Check inheritance - if (targetClass.isAssignableFrom(sourceClass)) { - return true; - } - - // Check primitive widening conversions - if (isPrimitiveWidening(sourceClass, targetClass)) { - return true; - } - - return false; - } - - /** - * Check if widening conversion is valid between primitives. - */ - private boolean isPrimitiveWidening(Class source, Class target) { - // byte -> short -> int -> long -> float -> double - // char -> int -> long -> float -> double - if (target == double.class || target == Double.class) { - return source == float.class || source == Float.class || - source == long.class || source == Long.class || - source == int.class || source == Integer.class || - source == short.class || source == Short.class || - source == byte.class || source == Byte.class || - source == char.class || source == Character.class; - } - if (target == float.class || target == Float.class) { - return source == long.class || source == Long.class || - source == int.class || source == Integer.class || - source == short.class || source == Short.class || - source == byte.class || source == Byte.class || - source == char.class || source == Character.class; - } - if (target == long.class || target == Long.class) { - return source == int.class || source == Integer.class || - source == short.class || source == Short.class || - source == byte.class || source == Byte.class || - source == char.class || source == Character.class; - } - if (target == int.class || target == Integer.class) { - return source == short.class || source == Short.class || - source == byte.class || source == Byte.class || - source == char.class || source == Character.class; - } - if (target == short.class || target == Short.class) { - return source == byte.class || source == Byte.class; - } - return false; - } - @Override public String toString() { StringBuilder sb = new StringBuilder("MethodCallInfo{"); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index 3fbd293d4..0327b3c4f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -16,16 +16,31 @@ private TypeChecker() {} // Utility class */ public static boolean isTypeCompatible(TypeInfo expected, TypeInfo actual) { if (expected == null) return true; // void can accept anything (shouldn't happen) - if (actual == null) return true; // null is compatible with any reference type + if (actual == null) return true; // Can't verify, assume compatible + + // Handle null literal - null is compatible with any reference type (non-primitive) + if ("".equals(actual.getFullName())) { + Class expectedClass = expected.getJavaClass(); + if (expectedClass != null && !expectedClass.isPrimitive()) { + return true; // null can be assigned to any reference type + } + // null cannot be assigned to primitive types + return false; + } String expectedName = expected.getSimpleName(); String actualName = actual.getSimpleName(); if (expectedName == null || actualName == null) return true; - // Exact match + // Exact match by simple name if (expectedName.equals(actualName)) return true; + // Exact match by full name + if (expected.getFullName() != null && actual.getFullName() != null) { + if (expected.getFullName().equals(actual.getFullName())) return true; + } + // Primitive widening conversions if (isNumericType(expectedName) && isNumericType(actualName)) { return canWiden(actualName, expectedName); @@ -33,10 +48,26 @@ public static boolean isTypeCompatible(TypeInfo expected, TypeInfo actual) { // Object type compatibility (check inheritance) if (expected.getJavaClass() != null && actual.getJavaClass() != null) { - return expected.getJavaClass().isAssignableFrom(actual.getJavaClass()); + Class expectedClass = expected.getJavaClass(); + Class actualClass = actual.getJavaClass(); + + // Direct assignability + if (expectedClass.isAssignableFrom(actualClass)) { + return true; + } + + // Primitive widening with Class objects + if (isPrimitiveWidening(actualClass, expectedClass)) { + return true; + } + + // Boxing/unboxing compatibility + if (isBoxingCompatible(actualClass, expectedClass)) { + return true; + } } - // Allow boxed/unboxed conversions + // Allow boxed/unboxed conversions by name if (isPrimitiveOrWrapper(expectedName) && isPrimitiveOrWrapper(actualName)) { return getUnboxedName(expectedName).equals(getUnboxedName(actualName)); } @@ -157,5 +188,68 @@ public static boolean isVoidType(TypeInfo type) { if (type == null) return true; return isVoidType(type.getSimpleName()); } + + /** + * Check for primitive widening conversions. + * byte -> short -> int -> long -> float -> double + * char -> int -> long -> float -> double + */ + private static boolean isPrimitiveWidening(Class from, Class to) { + if (!from.isPrimitive() || !to.isPrimitive()) { + return false; + } + + if (from == byte.class) { + return to == short.class || to == int.class || to == long.class || + to == float.class || to == double.class; + } + if (from == short.class || from == char.class) { + return to == int.class || to == long.class || to == float.class || to == double.class; + } + if (from == int.class) { + return to == long.class || to == float.class || to == double.class; + } + if (from == long.class) { + return to == float.class || to == double.class; + } + if (from == float.class) { + return to == double.class; + } + return false; + } + + /** + * Check for boxing/unboxing compatibility. + */ + private static boolean isBoxingCompatible(Class from, Class to) { + if (from.isPrimitive()) { + Class wrapper = getWrapperClassInternal(from); + if (wrapper != null && to.isAssignableFrom(wrapper)) { + return true; + } + } + if (to.isPrimitive()) { + Class wrapper = getWrapperClassInternal(to); + if (wrapper != null && wrapper.isAssignableFrom(from)) { + return true; + } + } + return false; + } + + /** + * Get the wrapper class for a primitive type. + */ + private static Class getWrapperClassInternal(Class primitive) { + if (primitive == boolean.class) return Boolean.class; + if (primitive == byte.class) return Byte.class; + if (primitive == char.class) return Character.class; + if (primitive == short.class) return Short.class; + if (primitive == int.class) return Integer.class; + if (primitive == long.class) return Long.class; + if (primitive == float.class) return Float.class; + if (primitive == double.class) return Double.class; + return null; + } } From 2ab48129b347b7aab81257689c55325bf7096dc9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 02:12:12 +0200 Subject: [PATCH 088/337] "null" is now a valid assignment/argument type --- .../gui/util/script/interpreter/ScriptDocument.java | 9 +++++++-- .../script/interpreter/expression/ExpressionNode.java | 4 +++- .../interpreter/expression/ExpressionTypeResolver.java | 4 ++-- .../util/script/interpreter/expression/TypeRules.java | 9 +++++++++ 4 files changed, 21 insertions(+), 5 deletions(-) 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 5a135bf6a..82dc31500 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 @@ -2277,7 +2277,7 @@ private TypeInfo resolveExpressionType(String expr, int position) { // Null literal if (expr.equals("null")) { - return null; // null is compatible with any reference type + return TypeInfo.unresolved("null", ""); // null is compatible with any reference type } // Numeric literals with precision checking @@ -2687,6 +2687,11 @@ private TypeInfo resolveExpressionWithOperators(String expr, int position) { ExpressionTypeResolver resolver = new ExpressionTypeResolver(context); TypeInfo result = resolver.resolve(expr); + // Special case: null literal type is "unresolved" but valid + if (result != null && "".equals(result.getFullName())) { + return result; + } + // If the expression resolver couldn't resolve it, fall back to simple resolution if (result == null || !result.isResolved()) { // Try the simple path in case the operator detection was a false positive @@ -2712,7 +2717,7 @@ public TypeInfo resolveIdentifier(String name) { return TypeInfo.fromPrimitive("boolean"); } if ("null".equals(name)) { - return null; + return TypeInfo.unresolved("null", ""); } // Try to resolve as a variable diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java index beb0397c0..85bcf1f30 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java @@ -59,7 +59,9 @@ public static class StringLiteralNode extends ExpressionNode { public static class NullLiteralNode extends ExpressionNode { public NullLiteralNode(int start, int end) { super(start, end); } - public TypeInfo resolveType(TypeResolverContext resolver) { return null; } + public TypeInfo resolveType(TypeResolverContext resolver) { + return TypeInfo.unresolved("null", ""); + } } public static class IdentifierNode extends ExpressionNode { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java index c07ffefcf..2a1e2bcf4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -51,7 +51,7 @@ private TypeInfo resolveNodeType(ExpressionNode node) { if (node instanceof ExpressionNode.BooleanLiteralNode) return TypeInfo.fromPrimitive("boolean"); if (node instanceof ExpressionNode.CharLiteralNode) return TypeInfo.fromPrimitive("char"); if (node instanceof ExpressionNode.StringLiteralNode) return TypeInfo.fromClass(String.class); - if (node instanceof ExpressionNode.NullLiteralNode) return null; + if (node instanceof ExpressionNode.NullLiteralNode) return TypeInfo.unresolved("null", ""); if (node instanceof ExpressionNode.IdentifierNode) { return context.resolveIdentifier(((ExpressionNode.IdentifierNode) node).getName()); @@ -132,7 +132,7 @@ public static ExpressionNode.TypeResolverContext createBasicContext() { return new ExpressionNode.TypeResolverContext() { public TypeInfo resolveIdentifier(String name) { if ("true".equals(name) || "false".equals(name)) return TypeInfo.fromPrimitive("boolean"); - if ("null".equals(name)) return null; + if ("null".equals(name)) return TypeInfo.unresolved("null", ""); return null; } public TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName) { return null; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java index 8b3fa8b17..2e6e71503 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java @@ -144,6 +144,15 @@ public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) if (thenType == null && elseType == null) return null; if (thenType == null) return elseType; if (elseType == null) return thenType; + + // Handle null literal type + boolean thenIsNull = "".equals(thenType.getFullName()); + boolean elseIsNull = "".equals(elseType.getFullName()); + + if (thenIsNull && elseIsNull) return thenType; // both null -> null + if (thenIsNull) return elseType; // null : T -> T + if (elseIsNull) return thenType; // T : null -> T + if (!thenType.isResolved()) return elseType.isResolved() ? elseType : null; if (!elseType.isResolved()) return thenType; From 7e25b1c85857963a73dd87df7876a9ecb6db728e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 15:05:39 +0200 Subject: [PATCH 089/337] Fetched best method overload according to arg types provided --- .../script/interpreter/ScriptDocument.java | 165 ++++++++++++++++-- .../script/interpreter/type/TypeInfo.java | 108 ++++++++++++ 2 files changed, 263 insertions(+), 10 deletions(-) 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 82dc31500..0ff134886 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 @@ -13,6 +13,7 @@ import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; @@ -1029,16 +1030,20 @@ private TypeInfo inferChainType(String firstIdent, int identStart, int dotPositi if (pos < text.length() && text.charAt(pos) == '(') { // Method call if (currentType.hasMethod(segment)) { - MethodInfo methodInfo = currentType.getMethodInfo(segment); + // Parse argument types + int closeParen = findMatchingParen(pos); + if (closeParen < 0) return null; + String argsText = text.substring(pos + 1, closeParen).trim(); + TypeInfo[] argTypes = parseArgumentTypes(argsText, pos + 1); + + MethodInfo methodInfo = currentType.getBestMethodOverload(segment, argTypes); currentType = (methodInfo != null) ? methodInfo.getReturnType() : null; + + // Skip to after the closing paren + pos = closeParen + 1; } else { return null; } - - // Skip to after the closing paren - int closeParen = findMatchingParen(pos); - if (closeParen < 0) return null; - pos = closeParen + 1; } else { // Field access if (currentType.hasField(segment)) { @@ -1906,13 +1911,17 @@ private void markMethodCalls(List marks) { if (receiverType != null) { if (receiverType.hasMethod(methodName)) { - resolvedMethod = receiverType.getMethodInfo(methodName); + + TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); + // Get best method overload based on argument types + resolvedMethod = receiverType.getBestMethodOverload(methodName, argTypes); + MethodCallInfo callInfo = new MethodCallInfo( methodName, nameStart, nameEnd, openParen, closeParen, arguments, receiverType, resolvedMethod, computedStaticAccess ); - // Only set expected type if this is the final expression (not followed by .field or .method) + // Set expected type for validation if this is the final expression if (!isFollowedByDot(closeParen)) { TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); if (expectedType != null) { @@ -1932,7 +1941,10 @@ private void markMethodCalls(List marks) { marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } else { if (isScriptMethod(methodName)) { - resolvedMethod = getScriptMethodInfo(methodName); + // Extract argument types from parsed arguments + + TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); + resolvedMethod = getScriptMethodInfo(methodName, argTypes); // Check if this is a method from a script type // Instance methods from script types cannot be called without a receiver @@ -2498,6 +2510,66 @@ private TypeInfo resolveReceiverChain(int methodNameStart) { return resolveExpressionType(receiverExpr, start); } + /** + * Parse argument types from a method call's argument list. + * @param argsText The text between parentheses (e.g., "20, 20" or "x, y.toString()") + * @param position The position in the source text + * @return Array of TypeInfo for each argument, or empty array if no arguments + */ + private TypeInfo[] parseArgumentTypes(String argsText, int position) { + if (argsText == null || argsText.trim().isEmpty()) { + return new TypeInfo[0]; + } + + // Split arguments by comma, respecting nested parentheses and strings + java.util.List args = new java.util.ArrayList<>(); + int depth = 0; + int start = 0; + boolean inString = false; + char stringChar = 0; + + for (int i = 0; i < argsText.length(); i++) { + char c = argsText.charAt(i); + + // Handle strings + if ((c == '"' || c == '\'') && (i == 0 || argsText.charAt(i-1) != '\\')) { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + continue; + } + + if (inString) continue; + + // Track parentheses depth + if (c == '(' || c == '[' || c == '{') { + depth++; + } else if (c == ')' || c == ']' || c == '}') { + depth--; + } else if (c == ',' && depth == 0) { + // Found argument separator at top level + args.add(argsText.substring(start, i).trim()); + start = i + 1; + } + } + + // Add the last argument + if (start < argsText.length()) { + args.add(argsText.substring(start).trim()); + } + + // Resolve each argument's type + TypeInfo[] argTypes = new TypeInfo[args.size()]; + for (int i = 0; i < args.size(); i++) { + argTypes[i] = resolveExpressionType(args.get(i), position); + } + + return argTypes; + } + /** * Helper class for chain segments (can be field access or method call). */ @@ -2762,7 +2834,7 @@ public TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeIn } if (targetType.hasMethod(methodName)) { - MethodInfo methodInfo = targetType.getMethodInfo(methodName); + MethodInfo methodInfo = targetType.getBestMethodOverload(methodName, argTypes); return (methodInfo != null) ? methodInfo.getReturnType() : null; } @@ -3076,6 +3148,79 @@ private MethodInfo getScriptMethodInfo(String methodName) { return null; } + /** + * Get the best matching script method overload based on argument types. + */ + private MethodInfo getScriptMethodInfo(String methodName, TypeInfo[] argTypes) { + List candidates = new ArrayList<>(); + + // Collect all methods with matching name + for (MethodInfo method : methods) { + if (method.getName().equals(methodName)) { + candidates.add(method); + } + } + + if (candidates.isEmpty()) { + return null; + } + + if (candidates.size() == 1) { + return candidates.get(0); + } + + // Use same 3-phase matching as TypeInfo.getBestMethodOverload + + // Phase 1: Look for exact match + for (MethodInfo method : candidates) { + List params = method.getParameters(); + if (params.size() != argTypes.length) { + continue; + } + + boolean exactMatch = true; + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getDeclaredType(); + TypeInfo argType = argTypes[i]; + + if (argType == null || paramType == null || !argType.equals(paramType)) { + exactMatch = false; + break; + } + } + + if (exactMatch) { + return method; + } + } + + // Phase 2: Look for compatible match (widening/autoboxing) + for (MethodInfo method : candidates) { + List params = method.getParameters(); + if (params.size() != argTypes.length) { + continue; + } + + boolean compatible = true; + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getDeclaredType(); + TypeInfo argType = argTypes[i]; + + if (argType != null && paramType != null && !TypeChecker.isTypeCompatible(paramType, argType)) { + compatible = false; + break; + } + } + + if (compatible) { + return method; + } + } + + // Phase 3: Fallback to first overload + return candidates.get(0); + } + /** * Check if a method belongs to a script-defined type (class/interface/enum). * Returns true if the method is defined inside a script type. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 8c9adadd0..f20c4e9d6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -272,6 +272,114 @@ public MethodInfo getMethodInfo(String methodName) { return null; } + /** + * Get all MethodInfo overloads for a method by name. + * Returns an empty list if not found. + */ + public java.util.List getAllMethodOverloads(String methodName) { + java.util.List overloads = new java.util.ArrayList<>(); + if (javaClass == null) return overloads; + try { + for (java.lang.reflect.Method m : javaClass.getMethods()) { + if (m.getName().equals(methodName)) { + overloads.add(MethodInfo.fromReflection(m, this)); + } + } + } catch (Exception e) { + // Security or linkage error + } + return overloads; + } + + /** + * Find the best matching method overload considering return type. + * First tries to find a match with compatible return type, then falls back to any match. + * + * @param methodName The name of the method + * @param expectedReturnType The expected return type (can be null) + * @return The best matching MethodInfo, or null if not found + */ + public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedReturnType) { + java.util.List overloads = getAllMethodOverloads(methodName); + if (overloads.isEmpty()) return null; + + // If no expected return type, return first overload + if (expectedReturnType == null) { + return overloads.get(0); + } + + // First pass: look for return type compatible overload + for (MethodInfo method : overloads) { + TypeInfo returnType = method.getReturnType(); + if (returnType != null && TypeChecker.isTypeCompatible(expectedReturnType, returnType)) { + return method; + } + } + // Second pass: return any overload (first one) + return overloads.get(0); + } + + /** + * Find the best matching method overload based on argument types. + * Uses Java's method resolution rules: exact match, then widening conversion, then autoboxing. + * + * @param methodName The name of the method + * @param argTypes The types of the arguments being passed + * @return The best matching MethodInfo, or null if not found + */ + public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) { + java.util.List overloads = getAllMethodOverloads(methodName); + if (overloads.isEmpty()) return null; + + // If no arguments provided, try to find zero-arg method + if (argTypes == null || argTypes.length == 0) { + for (MethodInfo method : overloads) { + if (method.getParameterCount() == 0) { + return method; + } + } + // Fall back to first overload if no zero-arg found + return overloads.get(0); + } + + // Phase 1: Try exact match + for (MethodInfo method : overloads) { + if (method.getParameterCount() == argTypes.length) { + boolean exactMatch = true; + java.util.List params = method.getParameters(); + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = argTypes[i]; + if (paramType == null || argType == null || !paramType.equals(argType)) { + exactMatch = false; + break; + } + } + if (exactMatch) return method; + } + } + + // Phase 2: Try compatible match (widening, autoboxing, subtyping) + for (MethodInfo method : overloads) { + if (method.getParameterCount() == argTypes.length) { + boolean compatible = true; + java.util.List params = method.getParameters(); + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = argTypes[i]; + if (paramType == null || argType == null || !TypeChecker.isTypeCompatible(paramType, argType)) { + compatible = false; + break; + } + } + if (compatible) return method; + } + } + + // Phase 3: Return first overload as fallback + return overloads.get(0); + } + /** * Get FieldInfo for a field by name. Returns null if not found. * Creates a synthetic FieldInfo based on reflection data. From 790cfae4767cdb0eb87572861fa3cc72f60d3042 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 15:16:40 +0200 Subject: [PATCH 090/337] resolveExpressionType now accounts for method arg types when resolving a method, to fetch the matching overload --- .../script/interpreter/ScriptDocument.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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 0ff134886..fe9f8ca45 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 @@ -2431,8 +2431,10 @@ private List parseExpressionChain(String expr) { // Check if followed by parentheses (method call) boolean isMethodCall = false; + String arguments = null; if (i < expr.length() && expr.charAt(i) == '(') { isMethodCall = true; + int argsStart = i + 1; // Skip to the matching closing paren int depth = 1; i++; @@ -2442,6 +2444,13 @@ private List parseExpressionChain(String expr) { else if (c == ')') depth--; i++; } + // Extract argument text (between parentheses) + int argsEnd = i - 1; // Position of closing paren + if (argsEnd > argsStart) { + arguments = expr.substring(argsStart, argsEnd); + } else { + arguments = ""; // Empty arguments + } } // Check if followed by array brackets (array access) @@ -2461,7 +2470,7 @@ private List parseExpressionChain(String expr) { } } - segments.add(new ChainSegment(name, start, i, isMethodCall)); + segments.add(new ChainSegment(name, start, i, isMethodCall, arguments)); // Skip whitespace while (i < expr.length() && Character.isWhitespace(expr.charAt(i))) { @@ -2578,12 +2587,14 @@ private static class ChainSegment { final int start; final int end; final boolean isMethodCall; + final String arguments; // The text between parentheses for method calls, or null for fields - ChainSegment(String name, int start, int end, boolean isMethodCall) { + ChainSegment(String name, int start, int end, boolean isMethodCall, String arguments) { this.name = name; this.start = start; this.end = end; this.isMethodCall = isMethodCall; + this.arguments = arguments; } } @@ -2596,9 +2607,13 @@ private TypeInfo resolveChainSegment(TypeInfo currentType, ChainSegment segment) } if (segment.isMethodCall) { - // Method call - get return type + // Method call - get return type with argument-based overload resolution if (currentType.hasMethod(segment.name)) { - MethodInfo methodInfo = currentType.getMethodInfo(segment.name); + // Parse argument types from the method call + TypeInfo[] argTypes = parseArgumentTypes(segment.arguments, segment.start); + + // Get the best matching overload based on argument types + MethodInfo methodInfo = currentType.getBestMethodOverload(segment.name, argTypes); return (methodInfo != null) ? methodInfo.getReturnType() : null; } return null; From 96d12c360d4c0a39bd6e7083ff8f6475af0281e4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 15:16:53 +0200 Subject: [PATCH 091/337] MethodInfo now stores it's java Method --- .../interpreter/hover/TokenHoverInfo.java | 26 +++---------------- .../script/interpreter/method/MethodInfo.java | 21 ++++++++------- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 8e186d454..a2a526572 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -533,15 +533,10 @@ private void extractMethodCallInfo(Token token) { } // Try to get actual Java method for more details - if (containingType != null && containingType.getJavaClass() != null && methodInfo != null) { - Class clazz = containingType.getJavaClass(); - Method javaMethod = findJavaMethod(clazz, methodInfo.getName(), methodInfo.getParameterCount()); - - if (javaMethod != null) { - buildMethodDeclaration(javaMethod, containingType); - extractJavadoc(javaMethod); - return; - } + if (methodInfo != null && methodInfo.getJavaMethod() != null) { + buildMethodDeclaration(methodInfo.getJavaMethod(), containingType); + extractJavadoc(methodInfo.getJavaMethod()); + return; } // Fallback to basic method info @@ -748,19 +743,6 @@ private void addSegment(String text, int color) { declaration.add(new TextSegment(text, color)); } - private Method findJavaMethod(Class clazz, String name, int paramCount) { - try { - for (Method m : clazz.getMethods()) { - if (m.getName().equals(name) && m.getParameterCount() == paramCount) { - return m; - } - } - } catch (Exception e) { - // Ignore - } - return null; - } - private void buildMethodDeclaration(Method method, TypeInfo containingType) { int mods = method.getModifiers(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index adee7fb33..23077d275 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -54,6 +54,7 @@ public enum ErrorType { private final boolean isDeclaration; // true if this is a declaration, false if it's a call private final int modifiers; // Java Modifier flags (e.g., Modifier.PUBLIC | Modifier.STATIC) private final String documentation; // Javadoc/comment documentation for this method + private final java.lang.reflect.Method javaMethod; // The Java reflection Method, if this was created from reflection // Error tracking for method declarations private ErrorType errorType = ErrorType.NONE; @@ -64,7 +65,7 @@ public enum ErrorType { private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, List parameters, int fullDeclarationOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration, - int modifiers, String documentation) { + int modifiers, String documentation, java.lang.reflect.Method javaMethod) { this.name = name; this.returnType = returnType; this.containingType = containingType; @@ -78,33 +79,34 @@ private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, this.isDeclaration = isDeclaration; this.modifiers = modifiers; this.documentation = documentation; + this.javaMethod = javaMethod; } // Factory methods public static MethodInfo declaration(String name, TypeInfo returnType, List params, int fullDeclOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd) { - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, 0, null); + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, 0, null, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, int fullDeclOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd, boolean isStatic) { int modifiers = isStatic ? Modifier.STATIC : 0; - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, null); + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, null, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, int fullDeclOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd, boolean isStatic, String documentation) { int modifiers = isStatic ? Modifier.STATIC : 0; - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation, null); } public static MethodInfo declaration(String name, TypeInfo returnType, List params, int fullDeclOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd, int modifiers, String documentation) { - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation); + return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation, null); } public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { @@ -114,7 +116,7 @@ public static MethodInfo call(String name, TypeInfo containingType, int paramCou for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, containingType, params, -1, -1, -1, -1, -1, resolved, false, 0, null); + return new MethodInfo(name, null, containingType, params, -1, -1, -1, -1, -1, resolved, false, 0, null, null); } public static MethodInfo unresolvedCall(String name, int paramCount) { @@ -122,7 +124,7 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { for (int i = 0; i < paramCount; i++) { params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER)); } - return new MethodInfo(name, null, null, params, -1, -1, -1, -1, -1, false, false, 0, null); + return new MethodInfo(name, null, null, params, -1, -1, -1, -1, -1, false, false, 0, null, null); } /** @@ -141,7 +143,7 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, null); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, null, method); } /** @@ -160,7 +162,7 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, true, modifiers, null); + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, true, modifiers, null, null); } // Getters @@ -169,6 +171,7 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T public TypeInfo getContainingType() { return containingType; } public List getParameters() { return Collections.unmodifiableList(parameters); } public int getParameterCount() { return parameters.size(); } + public Method getJavaMethod() { return javaMethod; } /** @deprecated Use getTypeOffset() or getNameOffset() instead */ @Deprecated public int getDeclarationOffset() { return typeOffset; } From bc05164209c22254bb03e342e444f3f45f3204d5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 16:12:44 +0200 Subject: [PATCH 092/337] Proper numeric promotion between java types for fetching the correct overload. i.ie Math.max(20f,20) checks if the second int can be promoted to the nearest matching overload max(float, float), and fetches it --- .../script/interpreter/type/TypeChecker.java | 61 +++++++++++++++++ .../script/interpreter/type/TypeInfo.java | 66 ++++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index 0327b3c4f..ac0aa901d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -119,6 +119,67 @@ public static int getNumericRank(String typeName) { } } + /** + * Check if a type is a numeric primitive (byte, short, int, long, float, double) + */ + public static boolean isNumericPrimitive(TypeInfo type) { + if (type == null || type.getJavaClass() == null) return false; + Class cls = type.getJavaClass(); + return cls == byte.class || cls == Byte.class || + cls == short.class || cls == Short.class || + cls == int.class || cls == Integer.class || + cls == long.class || cls == Long.class || + cls == float.class || cls == Float.class || + cls == double.class || cls == Double.class; + } + + /** + * Check if a numeric type can be promoted to a target numeric type. + * Follows Java's numeric promotion hierarchy: byte -> short -> int -> long -> float -> double + */ + public static boolean canPromoteNumeric(TypeInfo from, TypeInfo to) { + if (from == null || to == null || from.getJavaClass() == null || to.getJavaClass() == null) return false; + + Class fromClass = from.getJavaClass(); + Class toClass = to.getJavaClass(); + + // Unbox if necessary + if (fromClass == Byte.class) fromClass = byte.class; + if (fromClass == Short.class) fromClass = short.class; + if (fromClass == Integer.class) fromClass = int.class; + if (fromClass == Long.class) fromClass = long.class; + if (fromClass == Float.class) fromClass = float.class; + if (fromClass == Double.class) fromClass = double.class; + + if (toClass == Byte.class) toClass = byte.class; + if (toClass == Short.class) toClass = short.class; + if (toClass == Integer.class) toClass = int.class; + if (toClass == Long.class) toClass = long.class; + if (toClass == Float.class) toClass = float.class; + if (toClass == Double.class) toClass = double.class; + + // Get numeric ranks (higher = wider type) + int fromRank = getNumericRank(fromClass); + int toRank = getNumericRank(toClass); + + return fromRank >= 0 && toRank >= 0 && fromRank <= toRank; + } + + /** + * Get the numeric rank for promotion hierarchy. + * byte=0, short=1, int=2, long=3, float=4, double=5 + */ + public static int getNumericRank(Class cls) { + if (cls == byte.class) return 0; + if (cls == short.class) return 1; + if (cls == int.class) return 2; + if (cls == long.class) return 3; + if (cls == float.class) return 4; + if (cls == double.class) return 5; + return -1; + } + + /** * Check if the type name is a primitive or its wrapper type. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index f20c4e9d6..9fdf8dac4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -321,7 +321,7 @@ public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedRetu /** * Find the best matching method overload based on argument types. - * Uses Java's method resolution rules: exact match, then widening conversion, then autoboxing. + * Uses Java's method resolution rules: exact match, then numeric promotion, then widening conversion, then autoboxing. * * @param methodName The name of the method * @param argTypes The types of the arguments being passed @@ -359,7 +359,67 @@ public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) } } - // Phase 2: Try compatible match (widening, autoboxing, subtyping) + // Phase 2: Try numeric promotion match + // For methods with all numeric parameters, find the narrowest common type that all args can promote to + MethodInfo bestNumericMatch = null; + int bestNumericRank = Integer.MAX_VALUE; + + for (MethodInfo method : overloads) { + if (method.getParameterCount() == argTypes.length) { + java.util.List params = method.getParameters(); + + // Check if all parameters and arguments are numeric primitives + boolean allNumeric = true; + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = argTypes[i]; + if (paramType == null || argType == null || + !TypeChecker.isNumericPrimitive(paramType) || !TypeChecker.isNumericPrimitive(argType)) { + allNumeric = false; + break; + } + } + + if (allNumeric) { + // Check if all parameters are the same numeric type + TypeInfo commonParamType = params.get(0).getTypeInfo(); + boolean allParamsSame = true; + for (int i = 1; i < params.size(); i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + if (!paramType.equals(commonParamType)) { + allParamsSame = false; + break; + } + } + + // If all parameters are the same numeric type, check if args can promote to it + if (allParamsSame) { + boolean canPromote = true; + for (int i = 0; i < argTypes.length; i++) { + if (!TypeChecker.canPromoteNumeric(argTypes[i], commonParamType)) { + canPromote = false; + break; + } + } + + // If all args can promote, check if this is the narrowest match so far + if (canPromote) { + int paramRank = TypeChecker.getNumericRank(commonParamType.getJavaClass()); + if (paramRank < bestNumericRank) { + bestNumericRank = paramRank; + bestNumericMatch = method; + } + } + } + } + } + } + + if (bestNumericMatch != null) { + return bestNumericMatch; + } + + // Phase 3: Try compatible match (widening, autoboxing, subtyping) for (MethodInfo method : overloads) { if (method.getParameterCount() == argTypes.length) { boolean compatible = true; @@ -376,7 +436,7 @@ public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) } } - // Phase 3: Return first overload as fallback + // Phase 4: Return first overload as fallback return overloads.get(0); } From efd7d96ec7cc799814ca6b97443ce85bfaacf81a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 16:46:03 +0200 Subject: [PATCH 093/337] Casting completely functional --- .../script/interpreter/ScriptDocument.java | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) 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 fe9f8ca45..43e8db151 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 @@ -67,6 +67,11 @@ public class ScriptDocument { private static final Pattern FIELD_DECL_PATTERN = Pattern.compile( "\\b([A-Za-z_][a-zA-Z0-9_<>,[ \\t]\\[\\]]*)[ \\t]+([a-zA-Z_][a-zA-Z0-9_]*)[ \\t]*(=|;)"); private static final Pattern NEW_TYPE_PATTERN = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + + // Cast expression pattern: captures type name inside cast parentheses + // Matches: (Type), (Type[]), (pkg.Type), (Type) + private static final Pattern CAST_TYPE_PATTERN = Pattern.compile( + "\\(\\s*([A-Za-z_][a-zA-Z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][a-zA-Z0-9_]*)*)\\s*(?:<[^>]*>)?\\s*(\\[\\s*\\])*\\s*\\)\\s*(?=[a-zA-Z_\"'(\\d!~+-])"); // Function parameters (for JS-style scripts) private static final Pattern FUNC_PARAMS_PATTERN = Pattern.compile( @@ -1452,6 +1457,9 @@ private List buildMarks() { // Chained field accesses (e.g., mc.player.world, this.field) markChainedFieldAccesses(marks); + // Cast type expressions (e.g., (EntityPlayer) entity) + markCastTypes(marks); + // Imported class usages markImportedClassUsages(marks); @@ -2267,6 +2275,11 @@ private TypeInfo resolveExpressionType(String expr, int position) { return resolveExpressionWithOperators(expr, position); } + // Handle cast expressions: (Type)expr, ((Type)expr).method(), etc. + if (expr.startsWith("(")) { + return resolveCastOrParenthesizedExpression(expr, position); + } + // Invalid expressions starting with brackets if (expr.startsWith("[") || expr.startsWith("]")) { return null; // Invalid syntax @@ -2389,6 +2402,180 @@ private TypeInfo resolveExpressionType(String expr, int position) { return currentType; } + + /** + * Resolve a cast or parenthesized expression. + * Handles: + * - Simple casts: (Type) expr + * - Nested casts: ((Type) expr) + * - Cast chains: ((Type) expr).method() + * - Parenthesized expressions: (expr) + * + * @param expr The expression starting with '(' + * @param position The position in the source text + * @return The resolved type + */ + private TypeInfo resolveCastOrParenthesizedExpression(String expr, int position) { + if (!expr.startsWith("(")) return null; + + // Find the matching closing parenthesis for the first open paren + int depth = 0; + int closeParen = -1; + boolean inString = false; + char stringChar = 0; + + for (int i = 0; i < expr.length(); i++) { + char c = expr.charAt(i); + + // Handle string literals + if ((c == '"' || c == '\'') && (i == 0 || expr.charAt(i - 1) != '\\')) { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + continue; + } + if (inString) continue; + + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + closeParen = i; + break; + } + } + } + + if (closeParen < 0) return null; + + // Extract content inside parens + String insideParens = expr.substring(1, closeParen).trim(); + String afterParens = expr.substring(closeParen + 1).trim(); + + // Check if this is a cast expression + // A cast has a type name inside the parens (starts with uppercase or is primitive) + // and is followed by an expression (not an operator or nothing) + if (looksLikeCastType(insideParens) && looksLikeExpressionStart(afterParens)) { + // This is a cast: (Type) rest + TypeInfo castType = resolveType(insideParens); + if (castType != null && castType.isResolved()) { + // If there's more after the cast target, resolve the chain + // E.g., ((EntityPlayer) entity).worldObj + if (!afterParens.isEmpty()) { + // Check if afterParens starts with '.' for a method/field chain + if (afterParens.startsWith(".")) { + // Continue resolving the chain from the cast type + String chainExpr = afterParens.substring(1).trim(); + List segments = parseExpressionChain(chainExpr); + TypeInfo currentType = castType; + for (ChainSegment segment : segments) { + currentType = resolveChainSegment(currentType, segment); + if (currentType == null) return null; + } + return currentType; + } + } + return castType; + } + } + + // Not a cast - treat as parenthesized expression + // Could be ((expr).method()) or just (expr) + + // First, resolve what's inside the parens + TypeInfo innerType = resolveExpressionType(insideParens, position); + + // If there's a chain after the parens, continue resolving + if (innerType != null && !afterParens.isEmpty() && afterParens.startsWith(".")) { + String chainExpr = afterParens.substring(1).trim(); + List segments = parseExpressionChain(chainExpr); + TypeInfo currentType = innerType; + for (ChainSegment segment : segments) { + currentType = resolveChainSegment(currentType, segment); + if (currentType == null) return null; + } + return currentType; + } + + return innerType; + } + + /** + * Check if a string looks like a valid type name for a cast. + * Valid: EntityPlayer, int, String, java.lang.String, int[] + */ + private boolean looksLikeCastType(String s) { + if (s == null || s.isEmpty()) return false; + s = s.trim(); + + // Handle array types: Type[] or Type[][] + while (s.endsWith("[]")) { + s = s.substring(0, s.length() - 2).trim(); + } + + if (s.isEmpty()) return false; + + // Primitive types + switch (s) { + case "byte": case "short": case "int": case "long": + case "float": case "double": case "char": case "boolean": + return true; + } + + // Class type: starts with uppercase or is fully qualified + if (Character.isUpperCase(s.charAt(0))) return true; + + // Fully qualified name: contains dots and ends with uppercase segment + if (s.contains(".")) { + String[] parts = s.split("\\."); + String lastPart = parts[parts.length - 1]; + return !lastPart.isEmpty() && Character.isUpperCase(lastPart.charAt(0)); + } + + return false; + } + + /** + * Check if a string looks like the start of an expression (what follows a cast). + * Valid: identifier, (, new, literals, this, etc. + * Invalid: empty, operators like +, -, etc. + */ + private boolean looksLikeExpressionStart(String s) { + if (s == null || s.isEmpty()) return false; + s = s.trim(); + if (s.isEmpty()) return false; + + char first = s.charAt(0); + + // Parenthesis (nested cast or parenthesized expr) + if (first == '(') return true; + + // Identifier or keyword + if (Character.isJavaIdentifierStart(first)) return true; + + // String or char literal + if (first == '"' || first == '\'') return true; + + // Number literal (could start with digit or dot for .5) + if (Character.isDigit(first)) return true; + if (first == '.' && s.length() > 1 && Character.isDigit(s.charAt(1))) return true; + + // Unary operators followed by expression + if (first == '!' || first == '~') return true; + if ((first == '+' || first == '-') && s.length() > 1) { + // Could be unary +/- or ++/-- + char second = s.charAt(1); + if (second == first || Character.isDigit(second) || Character.isJavaIdentifierStart(second) || second == '(') { + return true; + } + } + + return false; + } /** * Parse an expression string into chain segments. @@ -4003,6 +4190,55 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st // Attach as the declaration assignment targetField.setDeclarationAssignment(info); } + + /** + * Mark types in cast expressions with their appropriate type color. + * Handles: (Type), (Type[]), (pkg.Type), etc. + */ + private void markCastTypes(List marks) { + Matcher m = CAST_TYPE_PATTERN.matcher(text); + + while (m.find()) { + String typeName = m.group(1); + int typeStart = m.start(1); + int typeEnd = m.end(1); + + // Skip if inside string or comment + if (isExcluded(typeStart)) continue; + + // Skip in import/package statements + if (isInImportOrPackage(typeStart)) continue; + + // Resolve the type + TypeInfo info = resolveType(typeName); + if (info != null && info.isResolved()) { + // Mark the type with its appropriate color + marks.add(new ScriptLine.Mark(typeStart, typeEnd, info.getTokenType(), info)); + } else { + // Unknown type - check if it's a primitive + if (isPrimitiveType(typeName)) { + marks.add(new ScriptLine.Mark(typeStart, typeEnd, TokenType.KEYWORD)); + } else { + // Mark as undefined type + marks.add(new ScriptLine.Mark(typeStart, typeEnd, TokenType.UNDEFINED_VAR)); + } + } + } + } + + /** + * Check if a type name is a primitive type. + */ + private boolean isPrimitiveType(String typeName) { + switch (typeName) { + case "byte": case "short": case "int": case "long": + case "float": case "double": case "char": case "boolean": + case "void": + return true; + default: + return false; + } + } private void markImportedClassUsages(List marks) { // Find uppercase identifiers followed by dot (static method calls, field access) From c46d61b22126eb1710fe07826203b6fee4081ab6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 17:02:25 +0200 Subject: [PATCH 094/337] Delegated Casting resolution to a helper class --- .../script/interpreter/ScriptDocument.java | 170 +------------- .../expression/CastExpressionResolver.java | 220 ++++++++++++++++++ 2 files changed, 231 insertions(+), 159 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java 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 43e8db151..0e58f0904 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 @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.interpreter.expression.CastExpressionResolver; import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionNode; import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionTypeResolver; import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; @@ -2405,6 +2406,7 @@ private TypeInfo resolveExpressionType(String expr, int position) { /** * Resolve a cast or parenthesized expression. + * Delegates to CastExpressionResolver helper class. * Handles: * - Simple casts: (Type) expr * - Nested casts: ((Type) expr) @@ -2416,166 +2418,16 @@ private TypeInfo resolveExpressionType(String expr, int position) { * @return The resolved type */ private TypeInfo resolveCastOrParenthesizedExpression(String expr, int position) { - if (!expr.startsWith("(")) return null; - - // Find the matching closing parenthesis for the first open paren - int depth = 0; - int closeParen = -1; - boolean inString = false; - char stringChar = 0; - - for (int i = 0; i < expr.length(); i++) { - char c = expr.charAt(i); - - // Handle string literals - if ((c == '"' || c == '\'') && (i == 0 || expr.charAt(i - 1) != '\\')) { - if (!inString) { - inString = true; - stringChar = c; - } else if (c == stringChar) { - inString = false; - } - continue; - } - if (inString) continue; - - if (c == '(') { - depth++; - } else if (c == ')') { - depth--; - if (depth == 0) { - closeParen = i; - break; - } - } - } - - if (closeParen < 0) return null; - - // Extract content inside parens - String insideParens = expr.substring(1, closeParen).trim(); - String afterParens = expr.substring(closeParen + 1).trim(); - - // Check if this is a cast expression - // A cast has a type name inside the parens (starts with uppercase or is primitive) - // and is followed by an expression (not an operator or nothing) - if (looksLikeCastType(insideParens) && looksLikeExpressionStart(afterParens)) { - // This is a cast: (Type) rest - TypeInfo castType = resolveType(insideParens); - if (castType != null && castType.isResolved()) { - // If there's more after the cast target, resolve the chain - // E.g., ((EntityPlayer) entity).worldObj - if (!afterParens.isEmpty()) { - // Check if afterParens starts with '.' for a method/field chain - if (afterParens.startsWith(".")) { - // Continue resolving the chain from the cast type - String chainExpr = afterParens.substring(1).trim(); - List segments = parseExpressionChain(chainExpr); - TypeInfo currentType = castType; - for (ChainSegment segment : segments) { - currentType = resolveChainSegment(currentType, segment); - if (currentType == null) return null; - } - return currentType; - } - } - return castType; - } - } - - // Not a cast - treat as parenthesized expression - // Could be ((expr).method()) or just (expr) - - // First, resolve what's inside the parens - TypeInfo innerType = resolveExpressionType(insideParens, position); - - // If there's a chain after the parens, continue resolving - if (innerType != null && !afterParens.isEmpty() && afterParens.startsWith(".")) { - String chainExpr = afterParens.substring(1).trim(); - List segments = parseExpressionChain(chainExpr); - TypeInfo currentType = innerType; - for (ChainSegment segment : segments) { - currentType = resolveChainSegment(currentType, segment); - if (currentType == null) return null; - } - return currentType; - } - - return innerType; - } - - /** - * Check if a string looks like a valid type name for a cast. - * Valid: EntityPlayer, int, String, java.lang.String, int[] - */ - private boolean looksLikeCastType(String s) { - if (s == null || s.isEmpty()) return false; - s = s.trim(); - - // Handle array types: Type[] or Type[][] - while (s.endsWith("[]")) { - s = s.substring(0, s.length() - 2).trim(); - } - - if (s.isEmpty()) return false; - - // Primitive types - switch (s) { - case "byte": case "short": case "int": case "long": - case "float": case "double": case "char": case "boolean": - return true; - } - - // Class type: starts with uppercase or is fully qualified - if (Character.isUpperCase(s.charAt(0))) return true; - - // Fully qualified name: contains dots and ends with uppercase segment - if (s.contains(".")) { - String[] parts = s.split("\\."); - String lastPart = parts[parts.length - 1]; - return !lastPart.isEmpty() && Character.isUpperCase(lastPart.charAt(0)); - } - - return false; - } - - /** - * Check if a string looks like the start of an expression (what follows a cast). - * Valid: identifier, (, new, literals, this, etc. - * Invalid: empty, operators like +, -, etc. - */ - private boolean looksLikeExpressionStart(String s) { - if (s == null || s.isEmpty()) return false; - s = s.trim(); - if (s.isEmpty()) return false; - - char first = s.charAt(0); - - // Parenthesis (nested cast or parenthesized expr) - if (first == '(') return true; - - // Identifier or keyword - if (Character.isJavaIdentifierStart(first)) return true; - - // String or char literal - if (first == '"' || first == '\'') return true; - - // Number literal (could start with digit or dot for .5) - if (Character.isDigit(first)) return true; - if (first == '.' && s.length() > 1 && Character.isDigit(s.charAt(1))) return true; - - // Unary operators followed by expression - if (first == '!' || first == '~') return true; - if ((first == '+' || first == '-') && s.length() > 1) { - // Could be unary +/- or ++/-- - char second = s.charAt(1); - if (second == first || Character.isDigit(second) || Character.isJavaIdentifierStart(second) || second == '(') { - return true; - } - } - - return false; + return CastExpressionResolver.resolveCastOrParenthesizedExpression( + expr, + position, + this::resolveType, + this::resolveExpressionType, + this::parseExpressionChain, + this::resolveChainSegment + ); } + /** * Parse an expression string into chain segments. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java new file mode 100644 index 000000000..f0d691174 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java @@ -0,0 +1,220 @@ +package noppes.npcs.client.gui.util.script.interpreter.expression; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.util.List; +import java.util.function.Function; + +/** + * Helper class for resolving cast expressions and distinguishing them from parenthesized expressions. + * Handles: + * - Simple casts: (Type) expr + * - Nested casts: ((Type) expr) + * - Cast chains: ((Type) expr).method() + * - Parenthesized expressions: (expr) + */ +public class CastExpressionResolver { + + /** + * Resolve a cast or parenthesized expression. + * + * @param expr The expression starting with '(' + * @param position The position in the source text + * @param resolveType Function to resolve type names to TypeInfo + * @param resolveExpression Function to resolve arbitrary expressions + * @param parseChain Function to parse expression chain + * @param resolveChainSegment Function to resolve chain segments + * @return The resolved type + */ + public static TypeInfo resolveCastOrParenthesizedExpression( + String expr, + int position, + Function resolveType, + TypeResolver resolveExpression, + ChainParser parseChain, + ChainSegmentResolver resolveChainSegment) { + + if (!expr.startsWith("(")) return null; + + // Find the matching closing parenthesis for the first open paren + int depth = 0; + int closeParen = -1; + boolean inString = false; + char stringChar = 0; + + for (int i = 0; i < expr.length(); i++) { + char c = expr.charAt(i); + + // Handle string literals + if ((c == '"' || c == '\'') && (i == 0 || expr.charAt(i - 1) != '\\')) { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + continue; + } + if (inString) continue; + + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + closeParen = i; + break; + } + } + } + + if (closeParen < 0) return null; + + // Extract content inside parens + String insideParens = expr.substring(1, closeParen).trim(); + String afterParens = expr.substring(closeParen + 1).trim(); + + // Check if this is a cast expression + // A cast has a type name inside the parens (starts with uppercase or is primitive) + // and is followed by an expression (not an operator or nothing) + if (looksLikeCastType(insideParens) && looksLikeExpressionStart(afterParens)) { + // This is a cast: (Type) rest + TypeInfo castType = resolveType.apply(insideParens); + if (castType != null && castType.isResolved()) { + // If there's more after the cast target, resolve the chain + // E.g., ((EntityPlayer) entity).worldObj + if (!afterParens.isEmpty()) { + // Check if afterParens starts with '.' for a method/field chain + if (afterParens.startsWith(".")) { + // Continue resolving the chain from the cast type + String chainExpr = afterParens.substring(1).trim(); + List segments = parseChain.parse(chainExpr); + TypeInfo currentType = castType; + for (T segment : segments) { + currentType = resolveChainSegment.resolve(currentType, segment); + if (currentType == null) return null; + } + return currentType; + } + } + return castType; + } + } + + // Not a cast - treat as parenthesized expression + // Could be ((expr).method()) or just (expr) + + // First, resolve what's inside the parens + TypeInfo innerType = resolveExpression.resolve(insideParens, position); + + // If there's a chain after the parens, continue resolving + if (innerType != null && !afterParens.isEmpty() && afterParens.startsWith(".")) { + String chainExpr = afterParens.substring(1).trim(); + List segments = parseChain.parse(chainExpr); + TypeInfo currentType = innerType; + for (T segment : segments) { + currentType = resolveChainSegment.resolve(currentType, segment); + if (currentType == null) return null; + } + return currentType; + } + + return innerType; + } + + /** + * Check if a string looks like a valid type name for a cast. + * Valid: EntityPlayer, int, String, java.lang.String, int[] + */ + public static boolean looksLikeCastType(String s) { + if (s == null || s.isEmpty()) return false; + s = s.trim(); + + // Handle array types: Type[] or Type[][] + while (s.endsWith("[]")) { + s = s.substring(0, s.length() - 2).trim(); + } + + if (s.isEmpty()) return false; + + // Primitive types + switch (s) { + case "byte": case "short": case "int": case "long": + case "float": case "double": case "char": case "boolean": + return true; + } + + // Class type: starts with uppercase or is fully qualified + if (Character.isUpperCase(s.charAt(0))) return true; + + // Fully qualified name: contains dots and ends with uppercase segment + if (s.contains(".")) { + String[] parts = s.split("\\."); + String lastPart = parts[parts.length - 1]; + return !lastPart.isEmpty() && Character.isUpperCase(lastPart.charAt(0)); + } + + return false; + } + + /** + * Check if a string looks like the start of an expression (what follows a cast). + * Valid: identifier, (, new, literals, this, etc. + * Invalid: empty, operators like +, -, etc. + */ + public static boolean looksLikeExpressionStart(String s) { + if (s == null || s.isEmpty()) return false; + s = s.trim(); + if (s.isEmpty()) return false; + + char first = s.charAt(0); + + // Parenthesis (nested cast or parenthesized expr) + if (first == '(') return true; + + // Identifier or keyword + if (Character.isJavaIdentifierStart(first)) return true; + + // String or char literal + if (first == '"' || first == '\'') return true; + + // Number literal (could start with digit or dot for .5) + if (Character.isDigit(first)) return true; + if (first == '.' && s.length() > 1 && Character.isDigit(s.charAt(1))) return true; + + // Unary operators followed by expression + if (first == '!' || first == '~') return true; + if ((first == '+' || first == '-') && s.length() > 1) { + // Could be unary +/- or ++/-- + char second = s.charAt(1); + if (second == first || Character.isDigit(second) || Character.isJavaIdentifierStart(second) || second == '(') { + return true; + } + } + + return false; + } + + /** + * Functional interface for resolving type expressions. + */ + @FunctionalInterface + public interface TypeResolver { + TypeInfo resolve(String expr, int position); + } + + /** + * Functional interface for parsing expression chains. + */ + @FunctionalInterface + public interface ChainParser { + List parse(String expr); + } + + /** + * Functional interface for resolving chain segments. + */ + @FunctionalInterface + public interface ChainSegmentResolver { + TypeInfo resolve(TypeInfo currentType, T segment); + } +} From c9e50f2cde7efac3e1ed190fdedf61acf55968ba Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 29 Dec 2025 17:05:24 +0200 Subject: [PATCH 095/337] Deprecated CastExpressionResolver as ExpressionResolver API already has all this built in --- .../gui/util/script/interpreter/ScriptDocument.java | 12 ++++-------- .../expression/CastExpressionResolver.java | 3 +++ 2 files changed, 7 insertions(+), 8 deletions(-) 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 0e58f0904..5e0f08da6 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 @@ -2272,13 +2272,9 @@ private TypeInfo resolveExpressionType(String expr, int position) { } // Check if expression contains operators - if so, use the full expression resolver - if (containsOperators(expr)) { - return resolveExpressionWithOperators(expr, position); - } - // Handle cast expressions: (Type)expr, ((Type)expr).method(), etc. - if (expr.startsWith("(")) { - return resolveCastOrParenthesizedExpression(expr, position); + if (containsOperators(expr) || expr.startsWith("(")) { + return resolveExpressionWithParserAPI(expr, position); } // Invalid expressions starting with brackets @@ -2802,10 +2798,10 @@ private boolean containsOperators(String expr) { } /** - * Resolve an expression that contains operators using the full expression parser. + * Resolve an expression that contains operators or casts using the full expression parser. * This handles all Java operators with proper precedence and type promotion rules. */ - private TypeInfo resolveExpressionWithOperators(String expr, int position) { + private TypeInfo resolveExpressionWithParserAPI(String expr, int position) { // Create a context that bridges to ScriptDocument's existing resolution methods ExpressionNode.TypeResolverContext context = createExpressionResolverContext(position); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java index f0d691174..d2b37c64a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/CastExpressionResolver.java @@ -11,7 +11,10 @@ * - Nested casts: ((Type) expr) * - Cast chains: ((Type) expr).method() * - Parenthesized expressions: (expr) + * + * @Deprecated ExpressionTypeResolver API has all this built in. */ +@Deprecated public class CastExpressionResolver { /** From 36e0e114fe52fdd82dedc68dddb9081747e5070f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 17:40:33 +0200 Subject: [PATCH 096/337] DONT FORGET TO TURN ON! Turned off script error logging --- src/main/java/noppes/npcs/controllers/ScriptContainer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index 3ea46b6a7..5b2c21cdd 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -150,7 +150,7 @@ public void run(ScriptEngine engine) { var14.printStackTrace(pw); } finally { String errorString = sw.getBuffer().toString().trim(); - this.appendConsole(errorString); + // this.appendConsole(errorString); pw.close(); } } @@ -225,7 +225,7 @@ public void run(String type, Object event) { errored = true; e.printStackTrace(pw); } finally { - appendConsole(sw.getBuffer().toString().trim()); + // appendConsole(sw.getBuffer().toString().trim()); pw.close(); Current = null; } From 0e2375e7f813805ad0fecb63b5a8d4a616085f55 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 18:07:30 +0200 Subject: [PATCH 097/337] Added error: "Cannot access non-static field from static context" --- .../script/interpreter/ScriptDocument.java | 52 +++++++++++++---- .../interpreter/type/ScriptTypeInfo.java | 26 +++++++-- .../script/interpreter/type/TypeInfo.java | 57 +++++++++++++++---- 3 files changed, 107 insertions(+), 28 deletions(-) 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 5e0f08da6..0f2f4730d 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 @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter; +import net.minecraft.client.Minecraft; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.interpreter.expression.CastExpressionResolver; import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionNode; @@ -3495,7 +3496,9 @@ private void markChainedFieldAccesses(List marks) { currentType = null; // We'll use globalFields for the next segment } else if (Character.isUpperCase(firstSegment.charAt(0))) { // Static field access like SomeClass.field - currentType = resolveType(firstSegment); + TypeInfo classType = resolveType(firstSegment); + // Class names are always in static context (can only access static members) + currentType = (classType != null) ? classType.asStaticContext() : null; } else { // Variable or field access if (firstIsPrecededByDot) { @@ -3504,7 +3507,8 @@ private void markChainedFieldAccesses(List marks) { TypeInfo receiverType = resolveReceiverChain(chainStart); if (receiverType != null && receiverType.hasField(firstSegment)) { FieldInfo varInfo = receiverType.getFieldInfo(firstSegment); - currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; + // Field values are always instances, even if field itself is static + currentType = (varInfo != null) ? varInfo.getTypeInfo().asInstanceContext() : null; } else { currentType = null; // Can't resolve } @@ -3512,7 +3516,8 @@ private void markChainedFieldAccesses(List marks) { // This is a variable starting the chain FieldInfo varInfo = resolveVariable(firstSegment, chainStart); if (varInfo != null) { - currentType = varInfo.getTypeInfo(); + // Variables are always instances + currentType = varInfo.getTypeInfo().asInstanceContext(); } else { currentType = null; } @@ -3544,7 +3549,8 @@ private void markChainedFieldAccesses(List marks) { FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], receiverType, fieldInfo, isLastSegment, isStaticAccessSeg); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + // Field values are always instances + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo().asInstanceContext() : null; } else { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); currentType = null; @@ -3554,29 +3560,51 @@ private void markChainedFieldAccesses(List marks) { if (globalFields.containsKey(segment)) { FieldInfo fieldInfo = globalFields.get(segment); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); - currentType = fieldInfo.getTypeInfo(); + // Field values are always instances + currentType = fieldInfo.getTypeInfo().asInstanceContext(); } else { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); currentType = null; } } else if (currentType != null && currentType.isResolved()) { // Check if this type has this field - //TODO: THIS MAY NEED TO HANDLE STATIC VS INSTANCE FIELDS WITH DIFFERENT RENDERING - if (currentType.hasField(segment)) { - FieldInfo fieldInfo = currentType.getFieldInfo(segment); - + // First check if field exists at all (ignoring static context) + FieldInfo fieldInfo = null; + boolean fieldExists = false; + boolean isStaticAccessError = false; + + // Check if field exists by temporarily using instance context + TypeInfo instanceContext = currentType.asInstanceContext(); + if (instanceContext.hasField(segment)) { + fieldExists = true; + // Now check if accessible in actual context + if (currentType.hasField(segment)) { + fieldInfo = currentType.getFieldInfo(segment); + } else { + // Field exists but not accessible in static context + isStaticAccessError = true; + fieldInfo = instanceContext.getFieldInfo(segment); + } + } + + if (fieldInfo != null && !isStaticAccessError) { // Check if this is the last segment and if it's in an assignment context // Create and mark FieldAccessInfo for this segment (last or intermediate) FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], currentType, fieldInfo, isLastSegment, isStaticAccessSeg); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - - // Update currentType for next segment - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + + // Update currentType for next segment - field values are always instances + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo().asInstanceContext() : null; /*TODO THIS ELSE OVERRIDES FOR CHAINED METHOD CALLS LIKE "Minecraft.getMinecraft()" Do we resolve this by increasing METHOD_CALL PRIORITY or how exactly? Think of the best fitting solution that doesnt regress other parts of the whole framework. */ + } else if (isStaticAccessError && fieldInfo != null) { + // Mark as error - trying to access non-static field from static context + String errorMsg = "Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'"; + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, errorMsg)); + currentType = null; // Can't continue resolving } else { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); currentType = null; // Can't continue resolving diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index 9f1f04a53..094257938 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -34,8 +34,8 @@ public class ScriptTypeInfo extends TypeInfo { private ScriptTypeInfo outerClass; private ScriptTypeInfo(String simpleName, String fullName, Kind kind, - int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { - super(simpleName, fullName, "", kind, null, true, null, true); + int declarationOffset, int bodyStart, int bodyEnd, int modifiers, boolean staticContext) { + super(simpleName, fullName, "", kind, null, true, null, staticContext, true); this.scriptClassName = simpleName; this.declarationOffset = declarationOffset; this.bodyStart = bodyStart; @@ -46,13 +46,13 @@ private ScriptTypeInfo(String simpleName, String fullName, Kind kind, // Factory method public static ScriptTypeInfo create(String simpleName, Kind kind, int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { - return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd, modifiers); + return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd, modifiers, false); } public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTypeInfo outer, int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { String fullName = outer.getFullName() + "$" + simpleName; - ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd, modifiers); + ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd, modifiers, false); inner.outerClass = outer; outer.innerClasses.add(inner); return inner; @@ -82,12 +82,26 @@ public void addField(FieldInfo field) { @Override public boolean hasField(String fieldName) { - return fields.containsKey(fieldName); + FieldInfo field = fields.get(fieldName); + if (field == null) return false; + + // In static context, only static fields are accessible + if (isStaticContext() && !field.isStatic()) { + return false; + } + return true; } @Override public FieldInfo getFieldInfo(String fieldName) { - return fields.get(fieldName); + FieldInfo field = fields.get(fieldName); + if (field == null) return null; + + // In static context, only static fields are accessible + if (isStaticContext() && !field.isStatic()) { + return null; // Non-static field not accessible from static context + } + return field; } public Map getFields() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 9fdf8dac4..b9dfa1c2c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -27,9 +27,10 @@ public enum Kind { private final Class javaClass; // The actual resolved Java class (null if unresolved) private final boolean resolved; // Whether this type was successfully resolved private final TypeInfo enclosingType; // For inner classes, the outer type (null if top-level) + private final boolean staticContext; // true if this is a class reference (can only access static members), false if instance private TypeInfo(String simpleName, String fullName, String packageName, - Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, boolean staticContext) { this.simpleName = simpleName; this.fullName = fullName; this.packageName = packageName; @@ -37,12 +38,13 @@ private TypeInfo(String simpleName, String fullName, String packageName, this.javaClass = javaClass; this.resolved = resolved; this.enclosingType = enclosingType; + this.staticContext = staticContext; } // Protected constructor for subclasses (like ScriptTypeInfo) protected TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, - @SuppressWarnings("unused") boolean subclass) { + boolean staticContext, @SuppressWarnings("unused") boolean subclass) { this.simpleName = simpleName; this.fullName = fullName; this.packageName = packageName; @@ -50,23 +52,24 @@ protected TypeInfo(String simpleName, String fullName, String packageName, this.javaClass = javaClass; this.resolved = resolved; this.enclosingType = enclosingType; + this.staticContext = staticContext; } // Factory methods public static TypeInfo resolved(String simpleName, String fullName, String packageName, Kind kind, Class javaClass) { - return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, null); + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, null, false); } public static TypeInfo resolvedInner(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, TypeInfo enclosing) { - return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, enclosing); + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, enclosing, false); } public static TypeInfo unresolved(String simpleName, String fullPath) { int lastDot = fullPath.lastIndexOf('.'); String pkg = lastDot > 0 ? fullPath.substring(0, lastDot) : ""; - return new TypeInfo(simpleName, fullPath, pkg, Kind.UNKNOWN, null, false, null); + return new TypeInfo(simpleName, fullPath, pkg, Kind.UNKNOWN, null, false, null, false); } public static TypeInfo fromClass(Class clazz) { @@ -99,7 +102,7 @@ public static TypeInfo fromClass(Class clazz) { enclosing = fromClass(clazz.getEnclosingClass()); } - return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing); + return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing, false); } /** @@ -118,7 +121,7 @@ public static TypeInfo fromPrimitive(String typeName) { case "double": primitiveClass = double.class; break; case "void": primitiveClass = void.class; break; } - return new TypeInfo(typeName, typeName, "", Kind.CLASS, primitiveClass, true, null); + return new TypeInfo(typeName, typeName, "", Kind.CLASS, primitiveClass, true, null, false); } // Getters @@ -130,6 +133,25 @@ public static TypeInfo fromPrimitive(String typeName) { public boolean isResolved() { return resolved; } public TypeInfo getEnclosingType() { return enclosingType; } public boolean isInnerClass() { return enclosingType != null; } + public boolean isStaticContext() { return staticContext; } + + /** + * Create a new TypeInfo with static context (for class references). + * Used when referencing a class name directly, which can only access static members. + */ + public TypeInfo asStaticContext() { + if (staticContext) return this; + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, true); + } + + /** + * Create a new TypeInfo with instance context (for object instances). + * Used when referencing an instance of a class, which can access both static and instance members. + */ + public TypeInfo asInstanceContext() { + if (!staticContext) return this; + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, false); + } /** * Get the appropriate TokenType for highlighting this type. @@ -237,13 +259,19 @@ public MethodInfo findConstructor(int argCount) { } /** - * Check if this type has a field with the given name. + * Check if this type has a field with the given name that is accessible in the current context. + * In static context (class reference), only static fields are accessible. + * In instance context, both static and instance fields are accessible. */ public boolean hasField(String fieldName) { if (javaClass == null) return false; try { for (java.lang.reflect.Field f : javaClass.getFields()) { if (f.getName().equals(fieldName)) { + // In static context, only static fields are accessible + if (staticContext && !java.lang.reflect.Modifier.isStatic(f.getModifiers())) { + return false; + } return true; } } @@ -441,7 +469,9 @@ public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) } /** - * Get FieldInfo for a field by name. Returns null if not found. + * Get FieldInfo for a field by name. Returns null if not found or not accessible. + * In static context (class reference), only static fields are accessible. + * In instance context, both static and instance fields are accessible. * Creates a synthetic FieldInfo based on reflection data. */ public FieldInfo getFieldInfo(String fieldName) { @@ -449,8 +479,15 @@ public FieldInfo getFieldInfo(String fieldName) { try { for (java.lang.reflect.Field f : javaClass.getFields()) { if (f.getName().equals(fieldName)) { + // In static context, only static fields are accessible + if (staticContext && !java.lang.reflect.Modifier.isStatic(f.getModifiers())) { + return null; // Non-static field not accessible from static context + } // Create a synthetic FieldInfo from reflection - return FieldInfo.fromReflection(f, this); + // Always return instance context for the field's type (field values are instances) + FieldInfo fieldInfo = FieldInfo.fromReflection(f, this); + // The field's type should be in instance context since we're accessing a value + return fieldInfo; } } } catch (Exception e) { From d6ca60b1219515e9029981e5a125a7c8b316f48f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 18:32:59 +0200 Subject: [PATCH 098/337] TokenErrorMessage impl --- .../script/interpreter/ScriptDocument.java | 5 ++-- .../util/script/interpreter/ScriptLine.java | 3 +++ .../interpreter/hover/TokenHoverInfo.java | 13 ++++++++++- .../util/script/interpreter/token/Token.java | 4 ++++ .../interpreter/token/TokenErrorMessage.java | 23 +++++++++++++++++++ 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenErrorMessage.java 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 0f2f4730d..a22da1a40 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 @@ -1,6 +1,5 @@ package noppes.npcs.client.gui.util.script.interpreter; -import net.minecraft.client.Minecraft; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.interpreter.expression.CastExpressionResolver; import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionNode; @@ -12,6 +11,7 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; @@ -3602,7 +3602,8 @@ private void markChainedFieldAccesses(List marks) { */ } else if (isStaticAccessError && fieldInfo != null) { // Mark as error - trying to access non-static field from static context - String errorMsg = "Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'"; + TokenErrorMessage errorMsg = TokenErrorMessage.from( + "Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'").clearOtherErrors(); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, errorMsg)); currentType = null; // Can't continue resolving } else { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 096df2e80..94af3cf33 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -6,6 +6,7 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -178,6 +179,8 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume token.setImportData((ImportData) mark.metadata); } else if (mark.metadata instanceof FieldAccessInfo) { token.setFieldAccessInfo((FieldAccessInfo) mark.metadata); + } else if (mark.metadata instanceof TokenErrorMessage) { + token.setErrorMessage((TokenErrorMessage) mark.metadata); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index a2a526572..52aff26cd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -7,6 +7,7 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -268,6 +269,17 @@ private void extractErrors(Token token) { } } } + + if(token.getType() == TokenType.UNDEFINED_VAR) + errors.add("Cannot resolve symbol '" + token.getText() + "'"); + + TokenErrorMessage msg = token.getErrorMessage(); + if (msg != null && !msg.getMessage().isEmpty()) { + if(msg.clearOtherErrors) + errors.clear(); + + errors.add(msg.getMessage()); + } } /** @@ -683,7 +695,6 @@ private void extractParameterInfo(Token token) { private void extractUndefinedInfo(Token token) { iconIndicator = "?"; addSegment(token.getText(), TokenType.UNDEFINED_VAR.getHexColor()); - errors.add("Cannot resolve symbol '" + token.getText() + "'"); } private void extractFieldInfoGeneric(Token token) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index be4bbc044..69b89c15b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -32,6 +32,8 @@ public class Token { private MethodCallInfo methodCallInfo; // For method calls with argument info private ImportData importData; // For import statements private FieldAccessInfo fieldAccessInfo; //For fields like Minecraft.getMinecraft.thePlayer + + private TokenErrorMessage errorMessage; // Navigation - set by ScriptLine @@ -125,6 +127,7 @@ public static Token undefined(String text, int start, int end) { public MethodCallInfo getMethodCallInfo() { return methodCallInfo; } public FieldAccessInfo getFieldAccessInfo() { return fieldAccessInfo; } public ImportData getImportData() { return importData; } + public TokenErrorMessage getErrorMessage() { return errorMessage; } public ScriptLine getParentLine() { return parentLine; } // ==================== SETTERS ==================== @@ -136,6 +139,7 @@ public static Token undefined(String text, int start, int end) { public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } + public void setErrorMessage(TokenErrorMessage message) { this.errorMessage = message; } public void setParentLine(ScriptLine line) { this.parentLine = line; } public void setPrev(Token prev) { this.prev = prev; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenErrorMessage.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenErrorMessage.java new file mode 100644 index 000000000..7b5115eb2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenErrorMessage.java @@ -0,0 +1,23 @@ +package noppes.npcs.client.gui.util.script.interpreter.token; + +public class TokenErrorMessage { + private final String message; + public boolean clearOtherErrors; + + public TokenErrorMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public static TokenErrorMessage from(String message) { + return new TokenErrorMessage(message); + } + + public TokenErrorMessage clearOtherErrors() { + this.clearOtherErrors = true; + return this; + } +} From d6b25127dff4ea2bbe1179967499a23bed265545 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 18:33:57 +0200 Subject: [PATCH 099/337] Commented out UNDEFINED_VAR hover info declaration --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 52aff26cd..e6a4f7c49 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -694,7 +694,7 @@ private void extractParameterInfo(Token token) { private void extractUndefinedInfo(Token token) { iconIndicator = "?"; - addSegment(token.getText(), TokenType.UNDEFINED_VAR.getHexColor()); + //addSegment(token.getText(), TokenType.UNDEFINED_VAR.getHexColor()); } private void extractFieldInfoGeneric(Token token) { From bd0044a2aada7275eb27d9f48254bbbfbea8abd2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 18:34:19 +0200 Subject: [PATCH 100/337] applyTokenMetadata --- .../util/script/interpreter/ScriptLine.java | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 94af3cf33..5b5a85332 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -157,32 +157,8 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume Token token = new Token(tokenText, tokenStart, tokenEnd, mark.type); // Attach metadata based on type - if (mark.metadata != null) { - if (mark.metadata instanceof TypeInfo) { - token.setTypeInfo((TypeInfo) mark.metadata); - } else if (mark.metadata instanceof MethodCallInfo) { - MethodCallInfo callInfo = (MethodCallInfo) mark.metadata; - if (callInfo.isConstructor()) { - token.setTypeInfo(callInfo.getReceiverType()); - token.setMethodInfo(callInfo.getResolvedMethod()); - } - token.setMethodCallInfo(callInfo); - } else if (mark.metadata instanceof FieldInfo.ArgInfo) { - FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) mark.metadata; - token.setFieldInfo(ctx.fieldInfo); - token.setMethodCallInfo(ctx.methodCallInfo); - } else if (mark.metadata instanceof FieldInfo) { - token.setFieldInfo((FieldInfo) mark.metadata); - } else if (mark.metadata instanceof MethodInfo) { - token.setMethodInfo((MethodInfo) mark.metadata); - } else if (mark.metadata instanceof ImportData) { - token.setImportData((ImportData) mark.metadata); - } else if (mark.metadata instanceof FieldAccessInfo) { - token.setFieldAccessInfo((FieldAccessInfo) mark.metadata); - } else if (mark.metadata instanceof TokenErrorMessage) { - token.setErrorMessage((TokenErrorMessage) mark.metadata); - } - } + if (mark.metadata != null) + applyTokenMetadata(token, mark.metadata); addToken(token); } @@ -200,6 +176,32 @@ public void buildTokensFromMarks(List marks, String fullText, ScriptDocume } } + public void applyTokenMetadata(Token token, Object metadata) { + if (metadata instanceof TypeInfo) { + token.setTypeInfo((TypeInfo) metadata); + } else if (metadata instanceof MethodCallInfo) { + MethodCallInfo callInfo = (MethodCallInfo) metadata; + if (callInfo.isConstructor()) { + token.setTypeInfo(callInfo.getReceiverType()); + token.setMethodInfo(callInfo.getResolvedMethod()); + } + token.setMethodCallInfo(callInfo); + } else if (metadata instanceof FieldInfo.ArgInfo) { + FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) metadata; + token.setFieldInfo(ctx.fieldInfo); + token.setMethodCallInfo(ctx.methodCallInfo); + } else if (metadata instanceof FieldInfo) { + token.setFieldInfo((FieldInfo) metadata); + } else if (metadata instanceof MethodInfo) { + token.setMethodInfo((MethodInfo) metadata); + } else if (metadata instanceof ImportData) { + token.setImportData((ImportData) metadata); + } else if (metadata instanceof FieldAccessInfo) { + token.setFieldAccessInfo((FieldAccessInfo) metadata); + } else if (metadata instanceof TokenErrorMessage) { + token.setErrorMessage((TokenErrorMessage) metadata); + } + } // ==================== INDENT GUIDES ==================== /** From 5899186445dc3c6cd71ce4baf1c992d25bb8124f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 18:51:22 +0200 Subject: [PATCH 101/337] Code optimization (for erorring static access of non static members) --- .../script/interpreter/ScriptDocument.java | 70 ++++++------------- .../interpreter/type/ScriptTypeInfo.java | 26 ++----- .../script/interpreter/type/TypeInfo.java | 57 +++------------ 3 files changed, 39 insertions(+), 114 deletions(-) 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 a22da1a40..46aa5bc97 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 @@ -3496,9 +3496,7 @@ private void markChainedFieldAccesses(List marks) { currentType = null; // We'll use globalFields for the next segment } else if (Character.isUpperCase(firstSegment.charAt(0))) { // Static field access like SomeClass.field - TypeInfo classType = resolveType(firstSegment); - // Class names are always in static context (can only access static members) - currentType = (classType != null) ? classType.asStaticContext() : null; + currentType = resolveType(firstSegment); } else { // Variable or field access if (firstIsPrecededByDot) { @@ -3507,8 +3505,7 @@ private void markChainedFieldAccesses(List marks) { TypeInfo receiverType = resolveReceiverChain(chainStart); if (receiverType != null && receiverType.hasField(firstSegment)) { FieldInfo varInfo = receiverType.getFieldInfo(firstSegment); - // Field values are always instances, even if field itself is static - currentType = (varInfo != null) ? varInfo.getTypeInfo().asInstanceContext() : null; + currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; } else { currentType = null; // Can't resolve } @@ -3516,8 +3513,7 @@ private void markChainedFieldAccesses(List marks) { // This is a variable starting the chain FieldInfo varInfo = resolveVariable(firstSegment, chainStart); if (varInfo != null) { - // Variables are always instances - currentType = varInfo.getTypeInfo().asInstanceContext(); + currentType = varInfo.getTypeInfo(); } else { currentType = null; } @@ -3549,8 +3545,7 @@ private void markChainedFieldAccesses(List marks) { FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], receiverType, fieldInfo, isLastSegment, isStaticAccessSeg); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - // Field values are always instances - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo().asInstanceContext() : null; + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; } else { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); currentType = null; @@ -3560,52 +3555,33 @@ private void markChainedFieldAccesses(List marks) { if (globalFields.containsKey(segment)) { FieldInfo fieldInfo = globalFields.get(segment); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); - // Field values are always instances - currentType = fieldInfo.getTypeInfo().asInstanceContext(); + currentType = fieldInfo.getTypeInfo(); } else { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); currentType = null; } } else if (currentType != null && currentType.isResolved()) { // Check if this type has this field - // First check if field exists at all (ignoring static context) - FieldInfo fieldInfo = null; - boolean fieldExists = false; - boolean isStaticAccessError = false; - - // Check if field exists by temporarily using instance context - TypeInfo instanceContext = currentType.asInstanceContext(); - if (instanceContext.hasField(segment)) { - fieldExists = true; - // Now check if accessible in actual context - if (currentType.hasField(segment)) { - fieldInfo = currentType.getFieldInfo(segment); + if (currentType.hasField(segment)) { + FieldInfo fieldInfo = currentType.getFieldInfo(segment); + + // If accessing from a class name (isStaticAccessSeg) and field is not static, that's an error + if (isStaticAccessSeg && fieldInfo != null && !fieldInfo.isStatic()) { + // Mark as error - trying to access non-static field from static context + TokenErrorMessage errorMsg = TokenErrorMessage + .from("Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'") + .clearOtherErrors(); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, errorMsg)); + currentType = null; // Can't continue resolving } else { - // Field exists but not accessible in static context - isStaticAccessError = true; - fieldInfo = instanceContext.getFieldInfo(segment); - } - } - - if (fieldInfo != null && !isStaticAccessError) { - // Check if this is the last segment and if it's in an assignment context - // Create and mark FieldAccessInfo for this segment (last or intermediate) - FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], currentType, fieldInfo, isLastSegment, isStaticAccessSeg); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); + // Valid access + FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], + currentType, fieldInfo, isLastSegment, isStaticAccessSeg); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - // Update currentType for next segment - field values are always instances - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo().asInstanceContext() : null; - - /*TODO THIS ELSE OVERRIDES FOR CHAINED METHOD CALLS LIKE "Minecraft.getMinecraft()" - Do we resolve this by increasing METHOD_CALL PRIORITY or how exactly? - Think of the best fitting solution that doesnt regress other parts of the whole framework. - */ - } else if (isStaticAccessError && fieldInfo != null) { - // Mark as error - trying to access non-static field from static context - TokenErrorMessage errorMsg = TokenErrorMessage.from( - "Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'").clearOtherErrors(); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, errorMsg)); - currentType = null; // Can't continue resolving + // Update currentType for next segment + currentType = (fieldInfo != null && fieldInfo.getTypeInfo() != null) ? fieldInfo.getTypeInfo() : null; + } } else { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); currentType = null; // Can't continue resolving diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index 094257938..9f1f04a53 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -34,8 +34,8 @@ public class ScriptTypeInfo extends TypeInfo { private ScriptTypeInfo outerClass; private ScriptTypeInfo(String simpleName, String fullName, Kind kind, - int declarationOffset, int bodyStart, int bodyEnd, int modifiers, boolean staticContext) { - super(simpleName, fullName, "", kind, null, true, null, staticContext, true); + int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { + super(simpleName, fullName, "", kind, null, true, null, true); this.scriptClassName = simpleName; this.declarationOffset = declarationOffset; this.bodyStart = bodyStart; @@ -46,13 +46,13 @@ private ScriptTypeInfo(String simpleName, String fullName, Kind kind, // Factory method public static ScriptTypeInfo create(String simpleName, Kind kind, int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { - return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd, modifiers, false); + return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd, modifiers); } public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTypeInfo outer, int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { String fullName = outer.getFullName() + "$" + simpleName; - ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd, modifiers, false); + ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd, modifiers); inner.outerClass = outer; outer.innerClasses.add(inner); return inner; @@ -82,26 +82,12 @@ public void addField(FieldInfo field) { @Override public boolean hasField(String fieldName) { - FieldInfo field = fields.get(fieldName); - if (field == null) return false; - - // In static context, only static fields are accessible - if (isStaticContext() && !field.isStatic()) { - return false; - } - return true; + return fields.containsKey(fieldName); } @Override public FieldInfo getFieldInfo(String fieldName) { - FieldInfo field = fields.get(fieldName); - if (field == null) return null; - - // In static context, only static fields are accessible - if (isStaticContext() && !field.isStatic()) { - return null; // Non-static field not accessible from static context - } - return field; + return fields.get(fieldName); } public Map getFields() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index b9dfa1c2c..9fdf8dac4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -27,10 +27,9 @@ public enum Kind { private final Class javaClass; // The actual resolved Java class (null if unresolved) private final boolean resolved; // Whether this type was successfully resolved private final TypeInfo enclosingType; // For inner classes, the outer type (null if top-level) - private final boolean staticContext; // true if this is a class reference (can only access static members), false if instance private TypeInfo(String simpleName, String fullName, String packageName, - Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, boolean staticContext) { + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { this.simpleName = simpleName; this.fullName = fullName; this.packageName = packageName; @@ -38,13 +37,12 @@ private TypeInfo(String simpleName, String fullName, String packageName, this.javaClass = javaClass; this.resolved = resolved; this.enclosingType = enclosingType; - this.staticContext = staticContext; } // Protected constructor for subclasses (like ScriptTypeInfo) protected TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, - boolean staticContext, @SuppressWarnings("unused") boolean subclass) { + @SuppressWarnings("unused") boolean subclass) { this.simpleName = simpleName; this.fullName = fullName; this.packageName = packageName; @@ -52,24 +50,23 @@ protected TypeInfo(String simpleName, String fullName, String packageName, this.javaClass = javaClass; this.resolved = resolved; this.enclosingType = enclosingType; - this.staticContext = staticContext; } // Factory methods public static TypeInfo resolved(String simpleName, String fullName, String packageName, Kind kind, Class javaClass) { - return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, null, false); + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, null); } public static TypeInfo resolvedInner(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, TypeInfo enclosing) { - return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, enclosing, false); + return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, true, enclosing); } public static TypeInfo unresolved(String simpleName, String fullPath) { int lastDot = fullPath.lastIndexOf('.'); String pkg = lastDot > 0 ? fullPath.substring(0, lastDot) : ""; - return new TypeInfo(simpleName, fullPath, pkg, Kind.UNKNOWN, null, false, null, false); + return new TypeInfo(simpleName, fullPath, pkg, Kind.UNKNOWN, null, false, null); } public static TypeInfo fromClass(Class clazz) { @@ -102,7 +99,7 @@ public static TypeInfo fromClass(Class clazz) { enclosing = fromClass(clazz.getEnclosingClass()); } - return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing, false); + return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing); } /** @@ -121,7 +118,7 @@ public static TypeInfo fromPrimitive(String typeName) { case "double": primitiveClass = double.class; break; case "void": primitiveClass = void.class; break; } - return new TypeInfo(typeName, typeName, "", Kind.CLASS, primitiveClass, true, null, false); + return new TypeInfo(typeName, typeName, "", Kind.CLASS, primitiveClass, true, null); } // Getters @@ -133,25 +130,6 @@ public static TypeInfo fromPrimitive(String typeName) { public boolean isResolved() { return resolved; } public TypeInfo getEnclosingType() { return enclosingType; } public boolean isInnerClass() { return enclosingType != null; } - public boolean isStaticContext() { return staticContext; } - - /** - * Create a new TypeInfo with static context (for class references). - * Used when referencing a class name directly, which can only access static members. - */ - public TypeInfo asStaticContext() { - if (staticContext) return this; - return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, true); - } - - /** - * Create a new TypeInfo with instance context (for object instances). - * Used when referencing an instance of a class, which can access both static and instance members. - */ - public TypeInfo asInstanceContext() { - if (!staticContext) return this; - return new TypeInfo(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, false); - } /** * Get the appropriate TokenType for highlighting this type. @@ -259,19 +237,13 @@ public MethodInfo findConstructor(int argCount) { } /** - * Check if this type has a field with the given name that is accessible in the current context. - * In static context (class reference), only static fields are accessible. - * In instance context, both static and instance fields are accessible. + * Check if this type has a field with the given name. */ public boolean hasField(String fieldName) { if (javaClass == null) return false; try { for (java.lang.reflect.Field f : javaClass.getFields()) { if (f.getName().equals(fieldName)) { - // In static context, only static fields are accessible - if (staticContext && !java.lang.reflect.Modifier.isStatic(f.getModifiers())) { - return false; - } return true; } } @@ -469,9 +441,7 @@ public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) } /** - * Get FieldInfo for a field by name. Returns null if not found or not accessible. - * In static context (class reference), only static fields are accessible. - * In instance context, both static and instance fields are accessible. + * Get FieldInfo for a field by name. Returns null if not found. * Creates a synthetic FieldInfo based on reflection data. */ public FieldInfo getFieldInfo(String fieldName) { @@ -479,15 +449,8 @@ public FieldInfo getFieldInfo(String fieldName) { try { for (java.lang.reflect.Field f : javaClass.getFields()) { if (f.getName().equals(fieldName)) { - // In static context, only static fields are accessible - if (staticContext && !java.lang.reflect.Modifier.isStatic(f.getModifiers())) { - return null; // Non-static field not accessible from static context - } // Create a synthetic FieldInfo from reflection - // Always return instance context for the field's type (field values are instances) - FieldInfo fieldInfo = FieldInfo.fromReflection(f, this); - // The field's type should be in instance context since we're accessing a value - return fieldInfo; + return FieldInfo.fromReflection(f, this); } } } catch (Exception e) { From cd506cbc7afbd96acb7f8f0369cbe98059decfef Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 18:54:14 +0200 Subject: [PATCH 102/337] Improved static access detection logic --- .../script/interpreter/ScriptDocument.java | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) 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 46aa5bc97..5e676f0ca 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 @@ -3494,12 +3494,13 @@ private void markChainedFieldAccesses(List marks) { // For 'this', we don't have a class context to resolve, but we can mark subsequent fields // as global fields if they exist in globalFields currentType = null; // We'll use globalFields for the next segment - } else if (Character.isUpperCase(firstSegment.charAt(0))) { - // Static field access like SomeClass.field - currentType = resolveType(firstSegment); } else { - // Variable or field access - if (firstIsPrecededByDot) { + // Try to resolve as a type first (class/interface/enum name) + TypeInfo typeCheck = resolveType(firstSegment); + if (typeCheck != null && typeCheck.isResolved()) { + // Static field access like SomeClass.field or scriptType.field + currentType = typeCheck; + } else if (firstIsPrecededByDot) { // This is a field access on a previous result (e.g., getMinecraft().thePlayer) // Resolve the receiver chain to get the type of what comes before the dot TypeInfo receiverType = resolveReceiverChain(chainStart); @@ -3529,11 +3530,14 @@ private void markChainedFieldAccesses(List marks) { continue; boolean isLastSegment = (i == chainSegments.size() - 1); - boolean isStaticAccessSeg = false; + boolean isStaticAccess = false; if (i - 1 >= 0) { String prev = chainSegments.get(i - 1); - if (!prev.isEmpty() && Character.isUpperCase(prev.charAt(0))) - isStaticAccessSeg = true; + // Check if previous segment is a class/type name (indicating static access) + TypeInfo prevType = resolveType(prev); + if (prevType != null && prevType.isResolved()) { + isStaticAccess = true; + } } // Handle the first segment if we're marking it (i.e., when preceded by dot) @@ -3542,7 +3546,7 @@ private void markChainedFieldAccesses(List marks) { TypeInfo receiverType = resolveReceiverChain(chainStart); if (receiverType != null && receiverType.hasField(segment)) { FieldInfo fieldInfo = receiverType.getFieldInfo(segment); - FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], receiverType, fieldInfo, isLastSegment, isStaticAccessSeg); + FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], receiverType, fieldInfo, isLastSegment, isStaticAccess); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; @@ -3566,7 +3570,7 @@ private void markChainedFieldAccesses(List marks) { FieldInfo fieldInfo = currentType.getFieldInfo(segment); // If accessing from a class name (isStaticAccessSeg) and field is not static, that's an error - if (isStaticAccessSeg && fieldInfo != null && !fieldInfo.isStatic()) { + if (isStaticAccess && fieldInfo != null && !fieldInfo.isStatic()) { // Mark as error - trying to access non-static field from static context TokenErrorMessage errorMsg = TokenErrorMessage .from("Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'") @@ -3576,7 +3580,7 @@ private void markChainedFieldAccesses(List marks) { } else { // Valid access FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], - currentType, fieldInfo, isLastSegment, isStaticAccessSeg); + currentType, fieldInfo, isLastSegment, isStaticAccess); marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); // Update currentType for next segment From b84ada3a30f218a407818d2c3600463119ffba04 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 19:08:30 +0200 Subject: [PATCH 103/337] Fixed class type detection using Character.isUpperCase in most contexts instead of more robust resolution --- .../gui/util/script/ClassPathFinder.java | 13 +++-- .../client/gui/util/script/FormatHelper.java | 11 ++-- .../script/interpreter/ScriptDocument.java | 50 +++++++++++++------ 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index e54f5d878..04f765cf5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -368,7 +368,8 @@ public ClassInfo resolveSimpleName(String simpleName, Map import if (p == null || p.isEmpty()) continue; String[] segs = p.split("\\."); String last = segs[segs.length - 1]; - boolean lastIsClass = last.length() > 0 && Character.isUpperCase(last.charAt(0)); + // Check if last segment is actually a class (try to resolve it) + boolean lastIsClass = last.length() > 0 && tryResolveClass(p) != null; // Try package wildcard: pkg.SimpleName if (!lastIsClass) { @@ -532,6 +533,8 @@ private List buildClassSegments(String[] segments, int classStartI private int findFirstUppercaseSegment(String[] segments) { for (int i = 0; i < segments.length; i++) { if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { + // Keep uppercase check here as it's a heuristic for package splitting + // This is about Java naming conventions, not actual type resolution return i; } } @@ -646,10 +649,10 @@ private void parseGenericTypesRecursive(String content, int baseOffset, } String typeName = content.substring(start, i); - // Only process if it looks like a type name (starts with uppercase) - if (Character.isUpperCase(typeName.charAt(0))) { - // Resolve the type - ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages); + // Try to resolve as an actual type + ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages); + if (info != null) { + // Only process if it actually resolves to a type ClassType classType = (info != null) ? info.type : ClassType.CLASS; results.add(new TypeOccurrence(baseOffset + start, baseOffset + i, typeName, classType)); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java index 5c534cc94..31a1dae9c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -625,9 +625,14 @@ private boolean isLikelyGeneric(String code, int pos, char bracket) { start--; } String before = code.substring(start, pos); - // Common generic types or capital letter start (type name) - if (!before.isEmpty() && Character.isUpperCase(before.charAt(0))) { - return true; + // Check if it's actually a type name (more robust than just checking uppercase) + if (!before.isEmpty()) { + // For FormatHelper, we don't have access to ScriptDocument's resolveType, + // so we check common patterns: uppercase start or known generic types + if (Character.isUpperCase(before.charAt(0)) || + before.equals("var") || before.equals("let") || before.equals("const")) { + return true; + } } } // Check if there's a matching > 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 5e676f0ca..7443cc308 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 @@ -999,9 +999,10 @@ private TypeInfo inferChainType(String firstIdent, int identStart, int dotPositi TypeInfo currentType = null; // Resolve the first segment - if (Character.isUpperCase(firstIdent.charAt(0))) { - // Static access like Event.player - currentType = resolveType(firstIdent); + TypeInfo typeCheck = resolveType(firstIdent); + if (typeCheck != null && typeCheck.isResolved()) { + // Static access like Event.player or scriptType.field + currentType = typeCheck; } else { // Variable access FieldInfo varInfo = resolveVariable(firstIdent, identStart); @@ -1804,8 +1805,9 @@ private void markGenericTypesRecursive(String content, int baseOffset, List 0 && Character.isUpperCase(name.charAt(0))) { - return resolveType(name); + if (name.length() > 0) { + TypeInfo typeCheck = resolveType(name); + if (typeCheck != null && typeCheck.isResolved()) { + return typeCheck; + } } return null; @@ -2986,9 +2998,15 @@ private TypeInfo resolveSimpleExpression(String expr, int position) { if (first.name.equals("this")) { currentType = findEnclosingScriptType(position); - } else if (Character.isUpperCase(first.name.charAt(0))) { - currentType = resolveType(first.name); - } else if (!first.isMethodCall) { + } else { + // Check if first segment is a type name + TypeInfo typeCheck = resolveType(first.name); + if (typeCheck != null && typeCheck.isResolved()) { + currentType = typeCheck; + } + } + + if (currentType == null && !first.isMethodCall) { FieldInfo varInfo = resolveVariable(first.name, position); if (varInfo != null) { currentType = varInfo.getTypeInfo(); From 91140d81bef39d64ad1d3783fdabc42c3d042048 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 19:17:28 +0200 Subject: [PATCH 104/337] Fixed class detection patterns only searching for capital case words --- .../client/gui/util/script/interpreter/ScriptDocument.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 7443cc308..6e5ced826 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 @@ -1699,7 +1699,7 @@ private void markTypeDeclarations(List marks) { // Pattern for type optionally followed by generics - we'll manually parse generics Pattern typeStart = Pattern.compile( "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + - "([A-Z][a-zA-Z0-9_]*)\\s*"); + "([a-zA-Z][a-zA-Z0-9_]*)\\s*"); Matcher m = typeStart.matcher(text); int searchFrom = 0; @@ -4117,7 +4117,7 @@ private boolean isPrimitiveType(String typeName) { private void markImportedClassUsages(List marks) { // Find uppercase identifiers followed by dot (static method calls, field access) - Pattern classUsage = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\s*\\."); + Pattern classUsage = Pattern.compile("\\b([A-Za-z][a-zA-Z0-9_]*)\\s*\\."); Matcher m = classUsage.matcher(text); while (m.find()) { @@ -4143,7 +4143,7 @@ private void markImportedClassUsages(List marks) { } // Also mark uppercase identifiers in type positions (new X(), X variable, etc.) - Pattern typeUsage = Pattern.compile("\\b(new\\s+)?([A-Z][a-zA-Z0-9_]*)(?:\\s*<[^>]*>)?\\s*(?:\\(|\\[|\\b[a-z])"); + Pattern typeUsage = Pattern.compile("\\b(new\\s+)?([A-Za-z][a-zA-Z0-9_]*)(?:\\s*<[^>]*>)?\\s*(?:\\(|\\[|\\b[a-z])"); Matcher tm = typeUsage.matcher(text); while (tm.find()) { From 8240cbae9e297668ed39649d6cd77efaa2a01891 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 19:37:41 +0200 Subject: [PATCH 105/337] Small refactor --- .../script/interpreter/hover/TokenHoverInfo.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index e6a4f7c49..50131346d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -569,9 +569,9 @@ private void extractMethodDeclInfo(Token token) { private void extractGlobalFieldInfo(Token token) { FieldInfo fieldInfo = token.getFieldInfo(); - FieldAccessInfo chainedField = token.getFieldAccessInfo(); - if (fieldInfo == null && chainedField != null) - fieldInfo = chainedField.getResolvedField(); + FieldAccessInfo accessInfo = token.getFieldAccessInfo(); + if (fieldInfo == null && accessInfo != null) + fieldInfo = accessInfo.getResolvedField(); if (fieldInfo == null) return; @@ -590,8 +590,8 @@ private void extractGlobalFieldInfo(Token token) { // Try to get modifiers from Java reflection if this is a chained field boolean foundModifiers = false; - if (chainedField != null && chainedField.getReceiverType() != null) { - TypeInfo receiverType = chainedField.getReceiverType(); + if (accessInfo != null && accessInfo.getReceiverType() != null) { + TypeInfo receiverType = accessInfo.getReceiverType(); if (receiverType.getJavaClass() != null) { try { Field javaField = receiverType.getJavaClass().getField(fieldInfo.getName()); @@ -617,8 +617,8 @@ private void extractGlobalFieldInfo(Token token) { // For Minecraft.getMinecraft.thePlayer // Return net.minecraft.client.Minecraft - if (chainedField != null) - pkg = chainedField.getReceiverType().getFullName(); + if (accessInfo != null) + pkg = accessInfo.getReceiverType().getFullName(); if (pkg != null && !pkg.isEmpty()) packageName = pkg; From d909e9e44ec668b0fbb066827f5e9ef7d06074a2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 19:38:00 +0200 Subject: [PATCH 106/337] Improved field modifier fetching in HoverInfo --- .../interpreter/hover/TokenHoverInfo.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 50131346d..9404b9313 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -603,14 +603,19 @@ private void extractGlobalFieldInfo(Token token) { } } - if (!foundModifiers && fieldInfo.getDeclarationOffset() >= 0) { - // Show modifiers from source if we have a declaration position - String modifiers = extractModifiersAtPosition(fieldInfo.getDeclarationOffset()); - if (modifiers != null && !modifiers.isEmpty()) { - addSegment(modifiers + " ", TokenType.MODIFIER.getHexColor()); - } + int modifiers = fieldInfo.getModifiers(); + if(modifiers !=0 && !foundModifiers) { + addFieldModifiers(modifiers); } +// if (!foundModifiers && fieldInfo.getDeclarationOffset() >= 0) { +// // Show modifiers from source if we have a declaration position +// String modifiers = extractModifiersAtPosition(fieldInfo.getDeclarationOffset()); +// if (modifiers != null && !modifiers.isEmpty()) { +// addSegment(modifiers + " ", TokenType.MODIFIER.getHexColor()); +// } +// } + if (declaredType != null) { // Show field's type package.ClassName for context String pkg = declaredType.getPackageName(); From e268a99f265f313afad2cc1dff35d84ccda1982a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 19:38:37 +0200 Subject: [PATCH 107/337] Added script type assignment errors to proper pool fetch methods --- .../util/script/interpreter/ScriptDocument.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 6e5ced826..a6584b640 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 @@ -4505,6 +4505,14 @@ public AssignmentInfo findAssignmentAtPosition(int position) { return assign; } } + + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + for (FieldInfo field : scriptType.getFields().values()) { + AssignmentInfo assign = field.findAssignmentAtPosition(position); + if (assign != null) + return assign; + } + } return null; } @@ -4539,6 +4547,14 @@ public List getAllErroredAssignments() { } } + + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + for (FieldInfo field : scriptType.getFields().values()) { + errored.addAll(field.getErroredAssignments()); + } + } + + // Include declaration errors (duplicates, etc.) errored.addAll(declarationErrors); From 9c1fadf493fe0bc4c83b8d9c19751ab10ac9f3e7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 20:03:12 +0200 Subject: [PATCH 108/337] Added missing FieldInfo code to ScriptTypes too --- .../script/interpreter/ScriptDocument.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) 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 a6584b640..7dd10ba14 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 @@ -420,7 +420,7 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { String typeNameRaw = fm.group(1).trim(); String fieldName = fm.group(2); - + String delimiter = fm.group(3); // Parse modifiers and strip them int modifiers = parseModifiers(typeNameRaw); String typeName = stripModifiers(typeNameRaw); @@ -440,8 +440,28 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { // Extract documentation before this field String documentation = extractDocumentationBefore(absPos); + // Extract initialization range if there's an '=' delimiter + int initStart = -1; + int initEnd = -1; + if ("=".equals(delimiter)) { + initStart = bodyStart + 1 + fm.start(3); // Absolute position of '=' + // Find the semicolon that ends this declaration + int searchPos = bodyStart + 1 + fm.end(3); + int depth = 0; // Track nested parens/brackets/braces + while (searchPos < text.length()) { + char c = text.charAt(searchPos); + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') depth--; + else if (c == ';' && depth == 0) { + initEnd = searchPos; // Position of ';' (exclusive) + break; + } + searchPos++; + } + } + TypeInfo fieldType = resolveType(typeName); - FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos, documentation, -1, -1, modifiers); + FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos, documentation, initStart, initEnd, modifiers); scriptType.addField(fieldInfo); } @@ -3764,6 +3784,12 @@ private void parseAssignments() { field.clearAssignments(); } } + // Also clear assignments in script type fields + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + for (FieldInfo field : scriptType.getFields().values()) { + field.clearAssignments(); + } + } externalFieldAssignments.clear(); // Pattern to find assignments: identifier = expression; @@ -4363,6 +4389,12 @@ private FieldInfo resolveVariable(String name, int position) { } } + // Check if we're inside a script type and look for fields there + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasField(name)) { + return enclosingType.getFieldInfo(name); + } + // Check global fields if (globalFields.containsKey(name)) { return globalFields.get(name); From 61fb092c82af5583bfc9d537ee7e54d05d41f5d3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 20:21:23 +0200 Subject: [PATCH 109/337] Non-static method call from static context validation --- .../script/interpreter/ScriptDocument.java | 34 ++++++++++--------- .../interpreter/method/MethodCallInfo.java | 1 + .../interpreter/type/ScriptTypeInfo.java | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) 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 7dd10ba14..80c452317 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 @@ -1939,7 +1939,6 @@ private void markMethodCalls(List marks) { // Resolve receiver using existing chain-based resolver and detect static access TypeInfo receiverType = resolveReceiverChain(nameStart); MethodInfo resolvedMethod = null; - boolean computedStaticAccess = isStaticAccessCall(nameStart); if (receiverType != null) { if (receiverType.hasMethod(methodName)) { @@ -1947,23 +1946,26 @@ private void markMethodCalls(List marks) { TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); // Get best method overload based on argument types resolvedMethod = receiverType.getBestMethodOverload(methodName, argTypes); - - MethodCallInfo callInfo = new MethodCallInfo( - methodName, nameStart, nameEnd, openParen, closeParen, - arguments, receiverType, resolvedMethod, computedStaticAccess - ); - - // Set expected type for validation if this is the final expression - if (!isFollowedByDot(closeParen)) { - TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); - if (expectedType != null) { - callInfo.setExpectedType(expectedType); + if (isStaticAccess && resolvedMethod != null && !resolvedMethod.isStatic()) { + TokenErrorMessage errorMsg = TokenErrorMessage + .from("Cannot call non-static method '" + methodName + "' from static context '" + receiverType.getSimpleName() + "'") + .clearOtherErrors(); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR, errorMsg)); + } else { + MethodCallInfo callInfo = new MethodCallInfo(methodName, nameStart, nameEnd, openParen, + closeParen, arguments, receiverType, resolvedMethod, isStaticAccess); + // Set expected type for validation if this is the final expression + if (!isFollowedByDot(closeParen)) { + TypeInfo expectedType = findExpectedTypeAtPosition(nameStart); + if (expectedType != null) { + callInfo.setExpectedType(expectedType); + } } + + callInfo.validate(); + methodCalls.add(callInfo); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } - - callInfo.validate(); - methodCalls.add(callInfo); - marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); } else { marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR)); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index 64438c8f9..8b7668fdf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -324,6 +324,7 @@ public void validate() { } // Check static/instance access (skip for constructors) + // NO LONGER CHECKED HERE, BUT DIRECTLY AT MARK CREATION if (!isConstructor && isStaticAccess && !resolvedMethod.isStatic()) { setError(ErrorType.STATIC_ACCESS_ERROR, "Cannot call instance method '" + methodName + "' on a class type"); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index 9f1f04a53..f1e9943bc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -126,7 +126,7 @@ public MethodInfo getMethodInfo(String methodName) { /** * Get all method overloads with the given name. */ - public List getMethodOverloads(String methodName) { + public List getAllMethodOverloads(String methodName) { return methods.getOrDefault(methodName, new ArrayList<>()); } From f0b45131054affa44b216e3df34a67498f638f10 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 30 Dec 2025 21:09:39 +0200 Subject: [PATCH 110/337] ExpressionTypeResolver API now checks resolved type against expected type. i.e. in: String s = true? true : ""; Both true and "" try to match s type String, and if any are incompatible it returns that so the error can be produced --- .../script/interpreter/ScriptDocument.java | 79 ++++++++-- .../expression/ExpressionTypeResolver.java | 8 + .../interpreter/expression/TypeRules.java | 137 +++++++++++++++++- 3 files changed, 207 insertions(+), 17 deletions(-) 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 80c452317..6bcbd86b8 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 @@ -18,6 +18,7 @@ import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; +import scala.annotation.meta.param; import java.util.*; import java.util.regex.Matcher; @@ -1930,8 +1931,8 @@ private void markMethodCalls(List marks) { continue; } - // Parse the arguments - List arguments = parseMethodArguments(openParen + 1, closeParen); + // Parse the arguments (first pass - without expected types for overload resolution) + List arguments = parseMethodArguments(openParen + 1, closeParen, null); // Check if this is a static access (Class.method() style) boolean isStaticAccess = isStaticAccessCall(nameStart); @@ -1946,12 +1947,18 @@ private void markMethodCalls(List marks) { TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); // Get best method overload based on argument types resolvedMethod = receiverType.getBestMethodOverload(methodName, argTypes); + if (isStaticAccess && resolvedMethod != null && !resolvedMethod.isStatic()) { TokenErrorMessage errorMsg = TokenErrorMessage .from("Cannot call non-static method '" + methodName + "' from static context '" + receiverType.getSimpleName() + "'") .clearOtherErrors(); marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.UNDEFINED_VAR, errorMsg)); } else { + // Second pass: Re-resolve arguments with expected parameter types if method was found + if (resolvedMethod != null && resolvedMethod.getParameters().size() == arguments.size()) + arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod); + + MethodCallInfo callInfo = new MethodCallInfo(methodName, nameStart, nameEnd, openParen, closeParen, arguments, receiverType, resolvedMethod, isStaticAccess); // Set expected type for validation if this is the final expression @@ -1979,6 +1986,11 @@ private void markMethodCalls(List marks) { TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); resolvedMethod = getScriptMethodInfo(methodName, argTypes); + + // Second pass: Re-resolve arguments with expected parameter types if method was found + if (resolvedMethod != null && resolvedMethod.getParameters().size() == arguments.size()) { + arguments = parseMethodArguments(openParen + 1, closeParen, resolvedMethod); + } // Check if this is a method from a script type // Instance methods from script types cannot be called without a receiver @@ -2144,7 +2156,15 @@ private int findMatchingParen(int openPos) { * Parse method arguments from the text between opening and closing parentheses. * Handles nested expressions, strings, and complex argument types. */ - private List parseMethodArguments(int start, int end) { + /** + * Parse method call arguments. + * + * @param start Start position of arguments (after opening paren) + * @param end End position of arguments (at closing paren) + * @param methodInfo Optional MethodInfo to provide expected parameter types for validation + * @return List of parsed arguments with resolved types + */ + private List parseMethodArguments(int start, int end, MethodInfo methodInfo) { List args = new ArrayList<>(); if (start >= end) { @@ -2173,7 +2193,14 @@ private List parseMethodArguments(int start, int end) { // Try to resolve the argument type // First check if this looks like a parameter declaration (Type varName) - TypeInfo argType = resolveArgumentType(argText, actualStart); + // Get expected parameter type if available + TypeInfo expectedParamType = null; + if (methodInfo != null && args.size() < methodInfo.getParameters().size()) { + FieldInfo parameter = methodInfo.getParameters().get(args.size()); + expectedParamType = parameter.getDeclaredType(); + } + + TypeInfo argType = resolveArgumentType(argText, actualStart, expectedParamType); args.add(new MethodCallInfo.Argument( argText, actualStart, actualEnd, argType, true, null @@ -2209,8 +2236,13 @@ private List parseMethodArguments(int start, int end) { /** * Resolve the type of a method call argument. * Handles both parameter declarations (Type varName) and expressions (variable.field()). + * + * @param argText The argument text + * @param position The position in the document + * @param expectedType Optional expected parameter type for validation + * @return The resolved type of the argument */ - private TypeInfo resolveArgumentType(String argText, int position) { + private TypeInfo resolveArgumentType(String argText, int position, TypeInfo expectedType) { argText = argText.trim(); // Check if this looks like a parameter declaration: "Type varName" @@ -2230,8 +2262,18 @@ private TypeInfo resolveArgumentType(String argText, int position) { return resolveType(typeName); } } - - // Otherwise, treat it as an expression + + // Otherwise, treat it as an expression with expected type context + if (expectedType != null) { + ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = expectedType; + try { + return resolveExpressionType(argText, position); + } finally { + ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null; + } + } + + // No expected type - resolve normally return resolveExpressionType(argText, position); } @@ -3968,8 +4010,14 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in } } - // Resolve the source type (RHS) - TypeInfo sourceType = resolveExpressionType(rhs, equalsPos + 1); + // Resolve the source type (RHS) with expected type context + TypeInfo sourceType; + ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = targetType; + try { + sourceType = resolveExpressionType(rhs, equalsPos + 1); + } finally { + ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null; + } // Determine if this is a script-defined field or external field FieldInfo finalTargetField = targetField; @@ -4068,7 +4116,15 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st } TypeInfo targetType = targetField.getDeclaredType(); - TypeInfo sourceType = resolveExpressionType(rhs, rhsStart); + + // Resolve the source type with expected type context + TypeInfo sourceType; + ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = targetType; + try { + sourceType = resolveExpressionType(rhs, rhsStart); + } finally { + ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null; + } // Create the assignment info // Declaration assignments should NOT check final status - this is the one place where final fields can be assigned @@ -4219,7 +4275,8 @@ private void markImportedClassUsages(List marks) { } // Parse arguments to find matching constructor - List arguments = parseMethodArguments(openParen + 1, closeParen); + List arguments = parseMethodArguments(openParen + 1, closeParen, + null); int argCount = arguments.size(); // Try to find matching constructor (may be null if not found) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java index 2a1e2bcf4..86aaacec9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -6,6 +6,14 @@ public class ExpressionTypeResolver { private final ExpressionNode.TypeResolverContext context; + /** + * Static field to track the expected/desired type for the current expression being resolved. + * This allows TypeRules to validate type compatibility in context (e.g., ternary operator branches + * must be compatible with the assignment target type). + * Should be set before calling resolve() and cleared after. + */ + public static TypeInfo CURRENT_EXPECTED_TYPE = null; + public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) { this.context = context; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java index 2e6e71503..ad1428088 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/TypeRules.java @@ -64,10 +64,11 @@ public static TypeInfo resolveBinaryOperatorType(OperatorType op, TypeInfo left, switch (op.getCategory()) { case ARITHMETIC: if (op == OperatorType.ADD && (isString(left) || isString(right))) { - return TypeInfo.fromClass(String.class); + return validateAgainstExpectedType(TypeInfo.fromClass(String.class)); } if (isNumeric(left) && isNumeric(right)) { - return binaryNumericPromotion(left, right); + TypeInfo promoted = binaryNumericPromotion(left, right); + return validateAgainstExpectedType(promoted); } return null; @@ -90,11 +91,13 @@ public static TypeInfo resolveBinaryOperatorType(OperatorType op, TypeInfo left, if (op == OperatorType.LEFT_SHIFT || op == OperatorType.RIGHT_SHIFT || op == OperatorType.UNSIGNED_RIGHT_SHIFT) { if (isIntegral(left)) { - return unaryNumericPromotion(left); + TypeInfo promoted = unaryNumericPromotion(left); + return validateAgainstExpectedType(promoted); } } if (isIntegral(left) && isIntegral(right)) { - return binaryNumericPromotion(left, right); + TypeInfo promoted = binaryNumericPromotion(left, right); + return validateAgainstExpectedType(promoted); } if (isBoolean(left) && isBoolean(right)) { return TypeInfo.fromPrimitive("boolean"); @@ -117,11 +120,17 @@ public static TypeInfo resolveUnaryOperatorType(OperatorType op, TypeInfo operan switch (op) { case UNARY_PLUS: case UNARY_MINUS: - if (isNumeric(operand)) return unaryNumericPromotion(operand); + if (isNumeric(operand)) { + TypeInfo promoted = unaryNumericPromotion(operand); + return validateAgainstExpectedType(promoted); + } return null; case BITWISE_NOT: - if (isIntegral(operand)) return unaryNumericPromotion(operand); + if (isIntegral(operand)) { + TypeInfo promoted = unaryNumericPromotion(operand); + return validateAgainstExpectedType(promoted); + } return null; case LOGICAL_NOT: @@ -139,6 +148,103 @@ public static TypeInfo resolveUnaryOperatorType(OperatorType op, TypeInfo operan return null; } } + + /** + * Check if sourceType can be assigned to targetType. + * Handles primitives, numeric conversions, null compatibility, and reference types. + */ + public static boolean isAssignmentCompatible(TypeInfo sourceType, TypeInfo targetType) { + if (sourceType == null || targetType == null) + return false; + if (!targetType.isResolved()) + return true; // Can't validate against unresolved type + + // Null literal can be assigned to any reference type + if ("".equals(sourceType.getFullName())) { + return !isPrimitive(targetType); + } + + if (!sourceType.isResolved()) + return false; + + // Exact match + if (sourceType.equals(targetType)) + return true; + if (sourceType.getFullName().equals(targetType.getFullName())) + return true; + + // Numeric conversions (widening primitive conversions) + String source = sourceType.getSimpleName(); + String target = targetType.getSimpleName(); + + // byte -> short, int, long, float, double + if ("byte".equals(source)) { + return "short".equals(target) || "int".equals(target) || "long".equals(target) || + "float".equals(target) || "double".equals(target); + } + // short -> int, long, float, double + if ("short".equals(source)) { + return "int".equals(target) || "long".equals(target) || + "float".equals(target) || "double".equals(target); + } + // char -> int, long, float, double + if ("char".equals(source)) { + return "int".equals(target) || "long".equals(target) || + "float".equals(target) || "double".equals(target); + } + // int -> long, float, double + if ("int".equals(source)) { + return "long".equals(target) || "float".equals(target) || "double".equals(target); + } + // long -> float, double + if ("long".equals(source)) { + return "float".equals(target) || "double".equals(target); + } + // float -> double + if ("float".equals(source)) { + return "double".equals(target); + } + + // For reference types, we'd need inheritance/interface checking + // For now, return false for incompatible types + return false; + } + + /** + * Check if a type is a primitive type. + */ + public static boolean isPrimitive(TypeInfo type) { + if (type == null || !type.isResolved()) + return false; + String name = type.getSimpleName(); + return "int".equals(name) || "long".equals(name) || "float".equals(name) || + "double".equals(name) || "byte".equals(name) || "short".equals(name) || + "char".equals(name) || "boolean".equals(name) || "void".equals(name); + } + + /** + * Validate a computed type against the current expected type context. + * If there's an expected type and the computed type is compatible, returns the expected type. + * If incompatible, returns the computed type so the error can be properly reported. + * If no expected type context, returns the computed type unchanged. + * + * @param computedType The type computed by normal type rules + * @return The type to use (either expectedType if compatible, or computedType) + */ + public static TypeInfo validateAgainstExpectedType(TypeInfo computedType) { + if (computedType == null) return null; + + TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE; + if (expectedType != null && expectedType.isResolved() && computedType.isResolved()) { + if (isAssignmentCompatible(computedType, expectedType)) { + return expectedType; + } + // Return computed type so error can be detected (incompatible with expected) + return computedType; + } + + return computedType; + } public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) { if (thenType == null && elseType == null) return null; @@ -155,7 +261,26 @@ public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) if (!thenType.isResolved()) return elseType.isResolved() ? elseType : null; if (!elseType.isResolved()) return thenType; + + // If there's an expected type context, validate both branches against it + TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE; + if (expectedType != null && expectedType.isResolved()) { + boolean thenCompatible = isAssignmentCompatible(thenType, expectedType); + boolean elseCompatible = isAssignmentCompatible(elseType, expectedType); + + // If both branches are compatible with expected type, return expected type + if (thenCompatible && elseCompatible) { + return expectedType; + } else if (!elseCompatible) { + // Return the incompatible type so error can be detected + return elseType; + } else { + // thenType is incompatible + return thenType; + } + } + // Normal ternary type resolution without expected type context if (thenType.equals(elseType)) return thenType; if (isNumeric(thenType) && isNumeric(elseType)) { return binaryNumericPromotion(thenType, elseType); From f3a7f3a0ad5dfc24dfb833729f85ff131b91abaa Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 01:20:59 +0200 Subject: [PATCH 111/337] ScriptLine.drawStringHex is now functional --- .../util/script/interpreter/ScriptLine.java | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 5b5a85332..d1d90209d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -299,31 +299,68 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { * @param renderer A renderer for drawing colored text and error underlines */ public void drawStringHex(int x, int y, HexColorRenderer renderer) { - int currentX = x; + // Build the complete text with all tokens and gaps, draw it as ONE string + // to match the spacing behavior of drawString which draws everything at once + StringBuilder fullText = new StringBuilder(); int lastIndex = 0; + + // Build segments with token position info + java.util.List segments = new java.util.ArrayList<>(); for (Token t : tokens) { int tokenStart = t.getGlobalStart() - globalStart; - // Draw any text before this token in default color + // Add any gap before this token if (tokenStart > lastIndex && tokenStart <= text.length()) { String gap = text.substring(lastIndex, tokenStart); - currentX = renderer.draw(gap, currentX, y, 0xFFFFFF); + segments.add(new TextSegment(fullText.length(), gap, 0xFFFFFFFF, false)); + fullText.append(gap); } - // Draw the colored token (with underline if flagged) - currentX = renderer.draw(t.getText(), currentX, y, t.getHexColor() - ); - + // Add the colored token + segments.add(new TextSegment(fullText.length(), t.getText(), t.getHexColor(), true)); + fullText.append(t.getText()); + lastIndex = tokenStart + t.getText().length(); } - // Draw any remaining text in default color + // Add any remaining text after the last token if (lastIndex < text.length()) { - renderer.draw(text.substring(lastIndex), currentX, y, 0xFFFFFF); + String remaining = text.substring(lastIndex); + segments.add(new TextSegment(fullText.length(), remaining, 0xFFFFFFFF, false)); + fullText.append(remaining); } - drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); + // Draw each segment at the correct position + // Calculate positions based on the full string to match drawString's spacing + for (TextSegment seg : segments) { + if (!seg.text.isEmpty()) { + // Get the width of everything before this segment + String prefix = fullText.substring(0, seg.startPos); + int prefixWidth = ClientProxy.Font.width(prefix); + int color = (seg.color & 0xFF000000) == 0 ? (0xFF000000 | seg.color) : seg.color; + + // Draw this segment at the correct position + ClientProxy.Font.drawString(seg.text, x + prefixWidth, y, color); + } + } + + drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); + } + + // Helper class to track text segments + private static class TextSegment { + final int startPos; + final String text; + final int color; + final boolean isToken; + + TextSegment(int startPos, String text, int color, boolean isToken) { + this.startPos = startPos; + this.text = text; + this.color = color; + this.isToken = isToken; + } } /** @@ -350,7 +387,7 @@ public interface HexColorRenderer { */ public static HexColorRenderer createDefaultHexRenderer() { return (text, x, y, hexColor) -> { - // Draw the text with hex color + // Draw the text with hex color and return the new X position directly from the renderer // Minecraft's font renderer uses ARGB, so add full alpha if not present int color = (hexColor & 0xFF000000) == 0 ? (0xFF000000 | hexColor) : hexColor; ClientProxy.Font.drawString(text, x, y, color); From 7f010f5e0c4bd9a872b1b91fb42f09adde2493ff Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 01:21:45 +0200 Subject: [PATCH 112/337] Removed no longer usued HexRenderer --- .../util/script/interpreter/ScriptLine.java | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index d1d90209d..cf6ba0b02 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -293,12 +293,8 @@ private void drawErrorUnderlines(int lineStartX, int baselineY) { * Draw this line with hex colors instead of Minecraft color codes. * More flexible but requires custom font rendering. * Also draws wavy underlines for tokens with errors. - * - * @param x X position - * @param y Y position - * @param renderer A renderer for drawing colored text and error underlines */ - public void drawStringHex(int x, int y, HexColorRenderer renderer) { + public void drawStringHex(int x, int y) { // Build the complete text with all tokens and gaps, draw it as ONE string // to match the spacing behavior of drawString which draws everything at once StringBuilder fullText = new StringBuilder(); @@ -362,40 +358,6 @@ private static class TextSegment { this.isToken = isToken; } } - - /** - * Functional interface for rendering text with hex colors and optional underlines. - */ - @FunctionalInterface - public interface HexColorRenderer { - /** - * Draw text at the specified position with the given color. - * - * @param text The text to draw - * @param x X position - * @param y Y position - * @param hexColor The text color in hex format - * @return The X position after drawing (for continuation) - */ - int draw(String text, int x, int y, int hexColor); - } - - /** - * Creates a default HexColorRenderer implementation using ClientProxy.Font. - * This renderer draws text with hex colors and curly underlines for errors. - * @return A HexColorRenderer that can be passed to drawStringHex - */ - public static HexColorRenderer createDefaultHexRenderer() { - return (text, x, y, hexColor) -> { - // Draw the text with hex color and return the new X position directly from the renderer - // Minecraft's font renderer uses ARGB, so add full alpha if not present - int color = (hexColor & 0xFF000000) == 0 ? (0xFF000000 | hexColor) : hexColor; - ClientProxy.Font.drawString(text, x, y, color); - int textWidth = ClientProxy.Font.width(text); - return x + textWidth; - }; - } - // ==================== UTILITIES ==================== /** From a7b43563cf6797720453c67c4dd1192da3e0cb95 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 01:22:08 +0200 Subject: [PATCH 113/337] Quick move --- .../util/script/interpreter/ScriptLine.java | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index cf6ba0b02..cb24663bf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -270,25 +270,6 @@ public void drawString(int x, int y, int defaultColor) { // Draw underlines for validation errors (method calls and field accesses) drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); } - - /** - * Draw underlines for all validation errors (method calls and field accesses) that intersect this line. - * Delegates to ErrorUnderlineRenderer for the actual rendering logic. - */ - private void drawErrorUnderlines(int lineStartX, int baselineY) { - if (parent == null) - return; - - ErrorUnderlineRenderer.drawErrorUnderlines( - parent, - lineStartX, - baselineY, - getText(), - getGlobalStart(), - getGlobalEnd() - ); - } - /** * Draw this line with hex colors instead of Minecraft color codes. * More flexible but requires custom font rendering. @@ -343,7 +324,26 @@ public void drawStringHex(int x, int y) { drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1); } - + + /** + * Draw underlines for all validation errors (method calls and field accesses) that intersect this line. + * Delegates to ErrorUnderlineRenderer for the actual rendering logic. + */ + private void drawErrorUnderlines(int lineStartX, int baselineY) { + if (parent == null) + return; + + ErrorUnderlineRenderer.drawErrorUnderlines( + parent, + lineStartX, + baselineY, + getText(), + getGlobalStart(), + getGlobalEnd() + ); + } + + // Helper class to track text segments private static class TextSegment { final int startPos; From 8089a5eef09c0f90b43c15eccf99214584e9669b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 02:36:10 +0200 Subject: [PATCH 114/337] Fully migrated to drawStringHex for Script Editor line rendering --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 9 +++++---- .../client/gui/util/script/interpreter/ScriptLine.java | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) 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 8c6ac7861..cb884ac96 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -849,10 +849,11 @@ public void drawTextBox(int xMouse, int yMouse) { } } int yPos = posY + stringYOffset; - - // data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); - scriptLine.drawString(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); - // scriptLine.drawStringHex(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, ScriptLine.createDefaultHexRenderer()); + + //data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + + //scriptLine.drawString(x+LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); + scriptLine.drawStringHex(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos); // Draw cursor: pause blinking while user is active recently boolean recentInput = selection.hadRecentInput(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index cb24663bf..c262a6877 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -319,6 +319,7 @@ public void drawStringHex(int x, int y) { // Draw this segment at the correct position ClientProxy.Font.drawString(seg.text, x + prefixWidth, y, color); + // Minecraft.getMinecraft().fontRenderer.drawString(seg.text, x + prefixWidth, y, color); } } From ef4438fcf4e8f837ebb7ea4aebe37ea50878f664 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 02:37:15 +0200 Subject: [PATCH 115/337] Optimised scrolling animation variables to fix the line text's stuttering at the end of scroll ease out --- .../noppes/npcs/client/gui/util/script/ScrollState.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java index 2649563ff..f8c92d64a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -23,9 +23,9 @@ public class ScrollState { private boolean clickScrolling = false; // Animation parameters - private static final double TAU = 0.1; // Time constant (~55ms feels snappy) - private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close - private static final double MAX_DT = 0.05; // Max delta time for stability + private static final double TAU = 0.15; // Time constant (~55ms feels snappy) + private static final double SNAP_THRESHOLD = 0.1; // Snap to target when this close (increased to avoid microstutters) + private static final double MAX_DT = 0.1; // Max delta time for stability /** * Reset scroll state to initial values From 6c0a2caa7e875f401ccad9bc2d3e63e89a415bef Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 13:30:01 +0200 Subject: [PATCH 116/337] Quick re-organisation --- .../gui/util/script/interpreter/ScriptDocument.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 6bcbd86b8..14a962d03 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 @@ -84,24 +84,23 @@ public class ScriptDocument { private String text = ""; private final List lines = new ArrayList<>(); + private final List imports = new ArrayList<>(); - private final List methods = new ArrayList<>(); - private final Map globalFields = new HashMap<>(); private final Set wildcardPackages = new HashSet<>(); private Map importsBySimpleName = new HashMap<>(); - + // Script-defined types (classes, interfaces, enums defined in the script) private final Map scriptTypes = new HashMap<>(); + private final List methods = new ArrayList<>(); + private final Map globalFields = new HashMap<>(); // Local variables per method (methodStartOffset -> {varName -> FieldInfo}) private final Map> methodLocals = new HashMap<>(); // Method calls - stores all parsed method call information private final List methodCalls = new ArrayList<>(); - // Field accesses - stores all parsed field access information private final List fieldAccesses = new ArrayList<>(); - // Assignments to external fields (fields from reflection, not script-defined) private final List externalFieldAssignments = new ArrayList<>(); From 4ff47456b73652a9b7c4972c0845bc5c6fcd0e12 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 14:09:17 +0200 Subject: [PATCH 117/337] Detected bodyless Methods declared within interfaces/have abstract modifiers --- .../script/interpreter/ScriptDocument.java | 26 ++++++++++++++++--- .../script/interpreter/method/MethodInfo.java | 7 +++++ 2 files changed, 29 insertions(+), 4 deletions(-) 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 14a962d03..b9ab96c84 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 @@ -18,7 +18,6 @@ import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; -import scala.annotation.meta.param; import java.util.*; import java.util.regex.Matcher; @@ -596,7 +595,7 @@ private boolean isInsideNestedMethod(int position, int classBodyStart, int class private void parseMethodDeclarations() { Pattern methodWithBody = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*(\\{|;)"); Matcher m = methodWithBody.matcher(text); while (m.find()) { @@ -604,14 +603,22 @@ private void parseMethodDeclarations() { continue; String returnType = m.group(1); + + if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum") ||returnType.equals("new")) { + continue; + } + String methodName = m.group(2); String paramList = m.group(3); + String delimiter = m.group(4); + boolean bodyless = delimiter.equals(";"); - int bodyStart = text.indexOf('{', m.end() - 1); - int bodyEnd = findMatchingBrace(bodyStart); + int bodyStart = bodyless ? m.end() : text.indexOf('{', m.end() - 1); + int bodyEnd = bodyless ? m.end() : findMatchingBrace(bodyStart); if (bodyEnd < 0) bodyEnd = text.length(); + // Extract documentation before this method String documentation = extractDocumentationBefore(m.start()); @@ -638,6 +645,17 @@ private void parseMethodDeclarations() { modifiers, documentation ); + + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + if (scriptType.containsPosition(bodyStart)) { + // scriptType.addMethod(methodInfo); + + if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) + methodInfo.setBodyless(bodyless); + + break; + } + } // Validate the method (return statements, parameters) with type resolution String methodBodyText = bodyEnd > bodyStart + 1 ? text.substring(bodyStart + 1, bodyEnd) : ""; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 23077d275..80ba41796 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -56,6 +56,7 @@ public enum ErrorType { private final String documentation; // Javadoc/comment documentation for this method private final java.lang.reflect.Method javaMethod; // The Java reflection Method, if this was created from reflection + private boolean bodyless; // Error tracking for method declarations private ErrorType errorType = ErrorType.NONE; private String errorMessage; @@ -193,6 +194,8 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T public boolean isPrivate() { return Modifier.isPrivate(modifiers); } public boolean isProtected() { return Modifier.isProtected(modifiers); } public String getDocumentation() { return documentation; } + public boolean isBodyless() { return bodyless; } + public void setBodyless(boolean bodyless) { this.bodyless = bodyless; } /** * Check if a position is inside this method's body. @@ -427,6 +430,10 @@ public void validate(String methodBodyText, TypeResolver typeResolver) { // Validate parameters validateParameters(); + //No need to check for return types/statements + if(bodyless) + return; + // Validate return statement types FIRST if we have a type resolver // This ensures type errors are shown even if there's also a missing return if (typeResolver != null) { From 002d2a6f15154af153a7704ef2655bafe40c1a79 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 14:25:47 +0200 Subject: [PATCH 118/337] Optimised ScriptType member detection --- .../script/interpreter/ScriptDocument.java | 165 ++++-------------- 1 file changed, 38 insertions(+), 127 deletions(-) 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 b9ab96c84..8fde5234e 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 @@ -409,113 +409,6 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { String bodyText = text.substring(bodyStart + 1, Math.min(bodyEnd, text.length())); - // Parse field declarations - Pattern fieldPattern = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>,\\s\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); - Matcher fm = fieldPattern.matcher(bodyText); - while (fm.find()) { - int absPos = bodyStart + 1 + fm.start(); - if (isExcluded(absPos)) continue; - - String typeNameRaw = fm.group(1).trim(); - String fieldName = fm.group(2); - String delimiter = fm.group(3); - // Parse modifiers and strip them - int modifiers = parseModifiers(typeNameRaw); - String typeName = stripModifiers(typeNameRaw); - - // Skip if this looks like a method declaration - if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || - typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || - typeName.equals("new") || typeName.equals("throw")) { - continue; - } - - // Skip if inside a nested method body - if (isInsideNestedMethod(absPos, bodyStart, bodyEnd)) { - continue; - } - - // Extract documentation before this field - String documentation = extractDocumentationBefore(absPos); - - // Extract initialization range if there's an '=' delimiter - int initStart = -1; - int initEnd = -1; - if ("=".equals(delimiter)) { - initStart = bodyStart + 1 + fm.start(3); // Absolute position of '=' - // Find the semicolon that ends this declaration - int searchPos = bodyStart + 1 + fm.end(3); - int depth = 0; // Track nested parens/brackets/braces - while (searchPos < text.length()) { - char c = text.charAt(searchPos); - if (c == '(' || c == '[' || c == '{') depth++; - else if (c == ')' || c == ']' || c == '}') depth--; - else if (c == ';' && depth == 0) { - initEnd = searchPos; // Position of ';' (exclusive) - break; - } - searchPos++; - } - } - - TypeInfo fieldType = resolveType(typeName); - FieldInfo fieldInfo = FieldInfo.globalField(fieldName, fieldType, absPos, documentation, initStart, initEnd, modifiers); - scriptType.addField(fieldInfo); - } - - // Parse method declarations - Pattern methodPattern = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{"); - Matcher mm = methodPattern.matcher(bodyText); - while (mm.find()) { - int absPos = bodyStart + 1 + mm.start(); - if (isExcluded(absPos)) continue; - - String returnTypeName = mm.group(1); - String methodName = mm.group(2); - String paramList = mm.group(3); - - // Skip class/interface/enum keywords - if (returnTypeName.equals("class") || returnTypeName.equals("interface") || - returnTypeName.equals("enum")) { - continue; - } - - int methodBodyStart = bodyStart + 1 + mm.end() - 1; - int methodBodyEnd = findMatchingBrace(methodBodyStart); - if (methodBodyEnd < 0) methodBodyEnd = bodyEnd; - - // Extract documentation before this method - String documentation = extractDocumentationBefore(absPos); - - // Extract modifiers by scanning backwards in bodyText from match start - int modifiers = extractModifiersBackwards(mm.start() - 1, bodyText); - - TypeInfo returnType = resolveType(returnTypeName); - List params = parseParametersWithPositions(paramList, - bodyStart + 1 + mm.start(3)); - - // Calculate the three offsets - int typeOffset = bodyStart + 1 + mm.start(1); // Start of return type - int nameOffset = bodyStart + 1 + mm.start(2); // Start of method name - int fullDeclOffset = findFullDeclarationStart(mm.start(1), bodyText); - if (fullDeclOffset >= 0) { - fullDeclOffset += bodyStart + 1; - } else { - fullDeclOffset = typeOffset; - } - - MethodInfo methodInfo = MethodInfo.declaration( - methodName, returnType, params, fullDeclOffset, typeOffset, nameOffset, methodBodyStart, methodBodyEnd, modifiers, documentation); - - // Validate the method (including return type checking) - String methodBodyText = text.substring(methodBodyStart + 1, methodBodyEnd); - methodInfo.validate(methodBodyText, (expr, pos) -> resolveExpressionType(expr, pos)); - - scriptType.addMethod(methodInfo); - } - // Parse constructors (ClassName(params) { ... }) // Constructor pattern: ClassName matches the script type name, no return type String typeName = scriptType.getSimpleName(); @@ -646,22 +539,25 @@ private void parseMethodDeclarations() { documentation ); - for (ScriptTypeInfo scriptType : scriptTypes.values()) { - if (scriptType.containsPosition(bodyStart)) { - // scriptType.addMethod(methodInfo); - - if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) - methodInfo.setBodyless(bodyless); - + ScriptTypeInfo scriptType = null; + for (ScriptTypeInfo type : scriptTypes.values()) + if (type.containsPosition(bodyStart)) { + scriptType = type; break; } - } - + + // Validate the method (return statements, parameters) with type resolution String methodBodyText = bodyEnd > bodyStart + 1 ? text.substring(bodyStart + 1, bodyEnd) : ""; methodInfo.validate(methodBodyText, (expr, pos) -> resolveExpressionType(expr, pos)); - - methods.add(methodInfo); + + if (scriptType != null) { + if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) + methodInfo.setBodyless(bodyless); + + scriptType.addMethod(methodInfo); + } else + methods.add(methodInfo); } // Check for duplicate method declarations @@ -1129,16 +1025,31 @@ private void parseGlobalFields() { // Strip modifiers (public, private, protected, static, final, etc.) from type name String typeName = stripModifiers(typeNameRaw); + ScriptTypeInfo containingScriptType = null; + for (ScriptTypeInfo scriptType : scriptTypes.values()) + if (scriptType.containsPosition(position)) { + containingScriptType = scriptType; + break; + } + // Check if inside a method - if so, it's a local, not global boolean insideMethod = false; - for (MethodInfo method : methods) { - if (method.containsPosition(position)) { - insideMethod = true; - break; + if (containingScriptType != null && isInsideNestedMethod(position, containingScriptType.getBodyStart(), + containingScriptType.getBodyEnd())) + insideMethod = true; + else { + for (MethodInfo method : methods) { + if (method.containsPosition(position)) { + insideMethod = true; + break; + } } } - if (!insideMethod) { + + if (insideMethod) + continue; + // Extract documentation before this field String documentation = extractDocumentationBefore(m.start()); @@ -1165,8 +1076,11 @@ private void parseGlobalFields() { TypeInfo typeInfo = resolveType(typeName); FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation, initStart, initEnd, modifiers); + + if (containingScriptType != null) { + containingScriptType.addField(fieldInfo); // Check for duplicate declaration - if (globalFields.containsKey(fieldName)) { + } else if (globalFields.containsKey(fieldName)) { // Create a declaration error for this duplicate AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( fieldName, position, position + fieldName.length(), @@ -1175,9 +1089,6 @@ private void parseGlobalFields() { } else { globalFields.put(fieldName, fieldInfo); } - - - } } } From 20f1c7cb66d07e3d595fd39bf2846454b2744491 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 14:27:57 +0200 Subject: [PATCH 119/337] Detected duplicate methods for ScriptTypeInfo methods --- .../gui/util/script/interpreter/ScriptDocument.java | 7 +++++++ .../gui/util/script/interpreter/type/ScriptTypeInfo.java | 8 ++++++++ 2 files changed, 15 insertions(+) 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 8fde5234e..d84e1d401 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 @@ -636,6 +636,13 @@ private void checkDuplicateMethods() { int scopeDepth = calculateScopeDepth(method.getFullDeclarationOffset()); methodsByScope.computeIfAbsent(scopeDepth, k -> new ArrayList<>()).add(method); } + + for (ScriptTypeInfo type : scriptTypes.values()) { + for (MethodInfo method : type.getAllMethodsFlat()) { + int scopeDepth = calculateScopeDepth(method.getFullDeclarationOffset()); + methodsByScope.computeIfAbsent(scopeDepth, k -> new ArrayList<>()).add(method); + } + } // Check each scope for duplicates for (List scopeMethods : methodsByScope.values()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index f1e9943bc..e3b405b1a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -148,6 +148,14 @@ public Map> getMethods() { return new HashMap<>(methods); } + public List getAllMethodsFlat() { + List allMethods = new ArrayList<>(); + for (List overloads : methods.values()) { + allMethods.addAll(overloads); + } + return allMethods; + } + // ==================== CONSTRUCTOR MANAGEMENT ==================== public void addConstructor(MethodInfo constructor) { From c97de07ef7a39e8fb861391d24adc834708c1423 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 14:46:00 +0200 Subject: [PATCH 120/337] Added MISSING_BODY error for method declarations --- .../interpreter/ErrorUnderlineRenderer.java | 20 +++++++------- .../script/interpreter/ScriptDocument.java | 7 +++++ .../interpreter/hover/TokenHoverInfo.java | 27 ++++++++++--------- .../script/interpreter/method/MethodInfo.java | 6 +++++ 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java index 3f475452a..2f8edf488 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java @@ -111,7 +111,7 @@ public static void drawErrorUnderlines( } // Check all method declarations for errors - for (MethodInfo method : doc.getMethods()) { + for (MethodInfo method : doc.getAllMethods()) { if (!method.isDeclaration() || !method.hasError()) continue; @@ -124,21 +124,14 @@ public static void drawErrorUnderlines( } // Handle missing return error (underline the method name) - if (method.hasMissingReturnError()) { + else if (method.hasMissingReturnError()) { int methodNameStart = method.getNameOffset(); int methodNameEnd = methodNameStart + method.getName().length(); drawUnderlineForSpan(methodNameStart, methodNameEnd, lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); } - - // Handle duplicate method error (underline from full declaration start to closing paren) - if (method.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) { - drawUnderlineForSpan(method.getFullDeclarationOffset(), method.getDeclarationEnd(), - lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); - } - // Handle parameter errors - if (method.hasParameterErrors()) { + else if (method.hasParameterErrors()) { for (MethodInfo.ParameterError paramError : method.getParameterErrors()) { FieldInfo param = paramError.getParameter(); if (param == null || param.getDeclarationOffset() < 0) @@ -150,6 +143,13 @@ public static void drawErrorUnderlines( lineText, lineStart, lineEnd, ERROR_COLOR); } } + + // Handle all other errors + else if (method.hasError()) { + drawUnderlineForSpan(method.getFullDeclarationOffset(), method.getDeclarationEnd(), lineStartX, + baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); + } + } } 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 d84e1d401..d2e6d51d1 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 @@ -4482,6 +4482,13 @@ public List getMethods() { return Collections.unmodifiableList(methods); } + public List getAllMethods() { + List allMethods = new ArrayList<>(methods); + for (ScriptTypeInfo scriptType : scriptTypes.values()) + allMethods.addAll(scriptType.getAllMethodsFlat()); + + return allMethods; + } public List getMethodCalls() { return Collections.unmodifiableList(methodCalls); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 9404b9313..02e65fe1a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -232,18 +232,8 @@ private void extractErrors(Token token) { } } - // If hovering over duplicate method declaration, show the error - if (methodDecl.getErrorType() == MethodInfo.ErrorType.DUPLICATE_METHOD) { - int declStart = methodDecl.getFullDeclarationOffset(); - int declEnd = methodDecl.getDeclarationEnd(); - - if (tokenStart >= declStart && tokenEnd <= declEnd) { - errors.add(methodDecl.getErrorMessage()); - } - } - // If hovering over a parameter with an error, show that error - if (methodDecl.hasParameterErrors()) { + else if (methodDecl.hasParameterErrors()) { for (MethodInfo.ParameterError paramError : methodDecl.getParameterErrors()) { FieldInfo param = paramError.getParameter(); if (param != null && param.getDeclarationOffset() >= 0) { @@ -258,7 +248,7 @@ private void extractErrors(Token token) { } // If hovering over a return statement with a type error, show that error - if (methodDecl.hasReturnStatementErrors()) { + else if (methodDecl.hasReturnStatementErrors()) { for (MethodInfo.ReturnStatementError returnError : methodDecl.getReturnStatementErrors()) { int returnStart = returnError.getStartOffset(); int returnEnd = returnError.getEndOffset(); @@ -268,6 +258,17 @@ private void extractErrors(Token token) { } } } + + + // All other errors + else if (methodDecl.hasError()) { + int declStart = methodDecl.getFullDeclarationOffset(); + int declEnd = methodDecl.getDeclarationEnd(); + + if (tokenStart >= declStart && tokenEnd <= declEnd) { + errors.add(methodDecl.getErrorMessage()); + } + } } if(token.getType() == TokenType.UNDEFINED_VAR) @@ -337,7 +338,7 @@ private MethodInfo findMethodDeclarationContainingPosition(Token token) { ScriptDocument doc = line.getParent(); int tokenStart = token.getGlobalStart(); - for (MethodInfo method : doc.getMethods()) { + for (MethodInfo method : doc.getAllMethods()) { if (!method.isDeclaration()) continue; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 80ba41796..1d4092d7b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -34,6 +34,7 @@ public final class MethodInfo { public enum ErrorType { NONE, MISSING_RETURN, // Non-void method missing return statement + MISSING_BODY, // Non-void method missing return statement RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type VOID_METHOD_RETURNS_VALUE, // Void method returns a value DUPLICATE_METHOD, // Method with same signature already defined in scope @@ -433,6 +434,11 @@ public void validate(String methodBodyText, TypeResolver typeResolver) { //No need to check for return types/statements if(bodyless) return; + + if (bodyStart == bodyEnd && !isAbstract() && !isNative()) { + setError(ErrorType.MISSING_BODY, "Method must have a body or be declared abstract/native."); + return; + } // Validate return statement types FIRST if we have a type resolver // This ensures type errors are shown even if there's also a missing return From 6cdb222b673c555dbd25233867eabfc77d119e69 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 16:53:38 +0200 Subject: [PATCH 121/337] Added INTERFACE_METHOD_BODY error & improved logic --- .../script/interpreter/ScriptDocument.java | 39 +++++++------- .../script/interpreter/method/MethodInfo.java | 54 +++++-------------- .../script/interpreter/type/TypeInfo.java | 4 +- 3 files changed, 36 insertions(+), 61 deletions(-) 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 d2e6d51d1..0023724bd 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 @@ -448,7 +448,9 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { // Constructors don't have a return type, but we'll use the containing type as a marker MethodInfo constructorInfo = MethodInfo.declaration( typeName, - scriptType, // Return type is the type itself + scriptType, + // Return type is the type itself + scriptType, params, fullDeclOffset, typeOffset, @@ -456,8 +458,7 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { constructorBodyStart, constructorBodyEnd, modifiers, - documentation - ); + documentation); scriptType.addConstructor(constructorInfo); } } @@ -504,10 +505,10 @@ private void parseMethodDeclarations() { String methodName = m.group(2); String paramList = m.group(3); String delimiter = m.group(4); - boolean bodyless = delimiter.equals(";"); + boolean hasBody = delimiter.equals("{"); - int bodyStart = bodyless ? m.end() : text.indexOf('{', m.end() - 1); - int bodyEnd = bodyless ? m.end() : findMatchingBrace(bodyStart); + int bodyStart = !hasBody ? m.end() : text.indexOf('{', m.end() - 1); + int bodyEnd = !hasBody ? m.end() : findMatchingBrace(bodyStart); if (bodyEnd < 0) bodyEnd = text.length(); @@ -523,11 +524,19 @@ private void parseMethodDeclarations() { int nameOffset = m.start(2); // Start of method name int fullDeclOffset = findFullDeclarationStart(m.start(1), text); // Start including modifiers + ScriptTypeInfo scriptType = null; + for (ScriptTypeInfo type : scriptTypes.values()) + if (type.containsPosition(bodyStart)) { + scriptType = type; + break; + } + // Parse parameters with their actual positions List params = parseParametersWithPositions(paramList, m.start(3)); MethodInfo methodInfo = MethodInfo.declaration( methodName, + scriptType, resolveType(returnType), params, fullDeclOffset, @@ -539,25 +548,15 @@ private void parseMethodDeclarations() { documentation ); - ScriptTypeInfo scriptType = null; - for (ScriptTypeInfo type : scriptTypes.values()) - if (type.containsPosition(bodyStart)) { - scriptType = type; - break; - } - - - // Validate the method (return statements, parameters) with type resolution - String methodBodyText = bodyEnd > bodyStart + 1 ? text.substring(bodyStart + 1, bodyEnd) : ""; - methodInfo.validate(methodBodyText, (expr, pos) -> resolveExpressionType(expr, pos)); if (scriptType != null) { - if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) - methodInfo.setBodyless(bodyless); - scriptType.addMethod(methodInfo); } else methods.add(methodInfo); + + // Validate the method (return statements, parameters) with type resolution + String methodBodyText = bodyEnd > bodyStart + 1 ? text.substring(bodyStart + 1, bodyEnd) : ""; + methodInfo.validate(methodBodyText, hasBody, (expr, pos) -> resolveExpressionType(expr, pos)); } // Check for duplicate method declarations diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 1d4092d7b..5b64373bf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -33,7 +33,8 @@ public final class MethodInfo { */ public enum ErrorType { NONE, - MISSING_RETURN, // Non-void method missing return statement + MISSING_RETURN, // Non-void method missing return statement, + INTERFACE_METHOD_BODY, // Interface method has a body MISSING_BODY, // Non-void method missing return statement RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type VOID_METHOD_RETURNS_VALUE, // Void method returns a value @@ -57,7 +58,6 @@ public enum ErrorType { private final String documentation; // Javadoc/comment documentation for this method private final java.lang.reflect.Method javaMethod; // The Java reflection Method, if this was created from reflection - private boolean bodyless; // Error tracking for method declarations private ErrorType errorType = ErrorType.NONE; private String errorMessage; @@ -84,31 +84,10 @@ private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, this.javaMethod = javaMethod; } - // Factory methods - public static MethodInfo declaration(String name, TypeInfo returnType, List params, - int fullDeclOffset, int typeOffset, int nameOffset, - int bodyStart, int bodyEnd) { - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, 0, null, null); - } - - public static MethodInfo declaration(String name, TypeInfo returnType, List params, - int fullDeclOffset, int typeOffset, int nameOffset, - int bodyStart, int bodyEnd, boolean isStatic) { - int modifiers = isStatic ? Modifier.STATIC : 0; - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, null, null); - } - - public static MethodInfo declaration(String name, TypeInfo returnType, List params, - int fullDeclOffset, int typeOffset, int nameOffset, - int bodyStart, int bodyEnd, boolean isStatic, String documentation) { - int modifiers = isStatic ? Modifier.STATIC : 0; - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation, null); - } - - public static MethodInfo declaration(String name, TypeInfo returnType, List params, + public static MethodInfo declaration(String name, TypeInfo containingType, TypeInfo returnType, List params, int fullDeclOffset, int typeOffset, int nameOffset, int bodyStart, int bodyEnd, int modifiers, String documentation) { - return new MethodInfo(name, returnType, null, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation, null); + return new MethodInfo(name, returnType, containingType, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation, null); } public static MethodInfo call(String name, TypeInfo containingType, int paramCount) { @@ -195,8 +174,6 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T public boolean isPrivate() { return Modifier.isPrivate(modifiers); } public boolean isProtected() { return Modifier.isProtected(modifiers); } public String getDocumentation() { return documentation; } - public boolean isBodyless() { return bodyless; } - public void setBodyless(boolean bodyless) { this.bodyless = bodyless; } /** * Check if a position is inside this method's body. @@ -407,16 +384,7 @@ public void addReturnStatementError(int startOffset, int endOffset, String messa public interface TypeResolver { TypeInfo resolveExpression(String expression, int position); } - - /** - * Validate this method declaration. - * Checks for parameter errors and missing return statements. - * - * @param methodBodyText The text content of the method body (between { and }) - */ - public void validate(String methodBodyText) { - validate(methodBodyText, null); - } + /** * Validate this method declaration with type resolution for return statements. @@ -425,16 +393,22 @@ public void validate(String methodBodyText) { * @param methodBodyText The text content of the method body (between { and }) * @param typeResolver Optional callback to resolve expression types (for return type checking) */ - public void validate(String methodBodyText, TypeResolver typeResolver) { + public void validate(String methodBodyText, boolean hasBody, TypeResolver typeResolver) { if (!isDeclaration) return; // Validate parameters validateParameters(); - + boolean interfaceMember = containingType != null && containingType.isInterface(); + if (hasBody && (interfaceMember || isAbstract() || isNative())) { + setError(MethodInfo.ErrorType.INTERFACE_METHOD_BODY, "Interface or abstract methods cannot have a body"); + return; + } + //No need to check for return types/statements - if(bodyless) + if(interfaceMember) return; + //If no body and not interface or abstract/native if (bodyStart == bodyEnd && !isAbstract() && !isNative()) { setError(ErrorType.MISSING_BODY, "Method must have a body or be declared abstract/native."); return; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 9fdf8dac4..7e2b512c2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -130,7 +130,9 @@ public static TypeInfo fromPrimitive(String typeName) { public boolean isResolved() { return resolved; } public TypeInfo getEnclosingType() { return enclosingType; } public boolean isInnerClass() { return enclosingType != null; } - + public boolean isInterface() {return kind == Kind.INTERFACE;} + public boolean isEnum() {return kind == Kind.ENUM;} + /** * Get the appropriate TokenType for highlighting this type. */ From dfec28ccc89ebbc051482c83d386db35cfbdcb19 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 17:02:24 +0200 Subject: [PATCH 122/337] Switched method fetches to getAllMethods() --- .../interpreter/ErrorUnderlineRenderer.java | 2 +- .../script/interpreter/ScriptDocument.java | 33 ++++--------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java index 2f8edf488..db64cdfe7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java @@ -53,7 +53,7 @@ public static void drawErrorUnderlines( // Skip method declarations that were erroneously recorded as calls boolean isDeclaration = false; int methodStart = call.getMethodNameStart(); - for (MethodInfo mi : doc.getMethods()) { + for (MethodInfo mi : doc.getAllMethods()) { if (mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) { isDeclaration = true; break; 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 0023724bd..9227c745e 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 @@ -631,17 +631,11 @@ private void checkDuplicateMethods() { // Group methods by scope depth Map> methodsByScope = new HashMap<>(); - for (MethodInfo method : methods) { + for (MethodInfo method : getAllMethods()) { int scopeDepth = calculateScopeDepth(method.getFullDeclarationOffset()); methodsByScope.computeIfAbsent(scopeDepth, k -> new ArrayList<>()).add(method); } - - for (ScriptTypeInfo type : scriptTypes.values()) { - for (MethodInfo method : type.getAllMethodsFlat()) { - int scopeDepth = calculateScopeDepth(method.getFullDeclarationOffset()); - methodsByScope.computeIfAbsent(scopeDepth, k -> new ArrayList<>()).add(method); - } - } + // Check each scope for duplicates for (List scopeMethods : methodsByScope.values()) { @@ -744,7 +738,7 @@ private List parseParametersWithPositions(String paramList, int param } private void parseLocalVariables() { - for (MethodInfo method : methods) { + for (MethodInfo method : getAllMethods()) { Map locals = new HashMap<>(); methodLocals.put(method.getDeclarationOffset(), locals); @@ -1044,7 +1038,7 @@ private void parseGlobalFields() { containingScriptType.getBodyEnd())) insideMethod = true; else { - for (MethodInfo method : methods) { + for (MethodInfo method : getAllMethods()) { if (method.containsPosition(position)) { insideMethod = true; break; @@ -1818,7 +1812,7 @@ private void markMethodDeclarations(List marks) { // Find the corresponding MethodInfo created in parseMethodDeclarations int methodDeclStart = m.start(); MethodInfo methodInfo = null; - for (MethodInfo method : methods) { + for (MethodInfo method : getAllMethods()) { if (method.getDeclarationOffset() == methodDeclStart) { methodInfo = method; break; @@ -3324,26 +3318,13 @@ private void markVariables(List marks) { )); // First pass: mark method parameters in their declaration positions - for (MethodInfo method : methods) { + for (MethodInfo method : getAllMethods()) { for (FieldInfo param : method.getParameters()) { int pos = param.getDeclarationOffset(); String name = param.getName(); marks.add(new ScriptLine.Mark(pos, pos + name.length(), TokenType.PARAMETER, param)); } } - - // Mark constructor parameters from script types - for (ScriptTypeInfo scriptType : scriptTypes.values()) { - for (MethodInfo constructor : scriptType.getConstructors()) { - for (FieldInfo param : constructor.getParameters()) { - int pos = param.getDeclarationOffset(); - if (pos >= 0) { - String name = param.getName(); - marks.add(new ScriptLine.Mark(pos, pos + name.length(), TokenType.PARAMETER, param)); - } - } - } - } Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); @@ -4438,7 +4419,7 @@ private boolean isInImportOrPackage(int position) { } private MethodInfo findMethodAtPosition(int position) { - for (MethodInfo method : methods) { + for (MethodInfo method : getAllMethods()) { if (method.containsPosition(position)) { return method; } From 8e283b705c93af4a31dd38cb4b76bce602a0a20b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 17:04:43 +0200 Subject: [PATCH 123/337] Removed unused code --- .../interpreter/ScriptTextContainer.java | 43 ------------------- 1 file changed, 43 deletions(-) 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 f32bd4f7e..c2a26596b 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 @@ -175,47 +175,4 @@ public noppes.npcs.client.gui.util.script.interpreter.token.Token getInterpreter return line.getTokenAt(globalPosition); } - - /** - * Get method blocks for compatibility with existing code. - * Creates MethodBlock-like objects from the new MethodInfo. - */ - public List getMethodBlocks() { - if (document == null) { - return new ArrayList<>(); - } - List blocks = new ArrayList<>(); - for (MethodInfo method : document.getMethods()) { - blocks.add(new MethodBlockCompat(method)); - } - return blocks; - } - - /** - * Compatibility wrapper for MethodBlock. - */ - public static class MethodBlockCompat { - private final MethodInfo methodInfo; - public List parameters = new ArrayList<>(); - public List localVariables = new ArrayList<>(); - - public MethodBlockCompat(MethodInfo methodInfo) { - this.methodInfo = methodInfo; - for (FieldInfo param : methodInfo.getParameters()) { - parameters.add(param.getName()); - } - } - - public int getStartOffset() { return methodInfo.getDeclarationOffset(); } - public int getEndOffset() { return methodInfo.getBodyEnd(); } - - public boolean containsPosition(int position) { - return methodInfo.containsPosition(position); - } - - public boolean isLocalDeclaredAtPosition(String varName, int position) { - // Simplified - would need local variable tracking for full support - return localVariables.contains(varName); - } - } } From 71b4a6fdc4225743eb2f202a84508a9c0dada7cc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 18:53:28 +0200 Subject: [PATCH 124/337] Huge ScriptType extends/implements back-end --- .../client/gui/util/GuiScriptTextArea.java | 149 ++++- .../noppes/npcs/client/gui/util/GuiUtil.java | 26 + .../script/interpreter/ScriptDocument.java | 514 +++++++++++++++++- .../interpreter/hover/TokenHoverInfo.java | 72 ++- .../script/interpreter/method/MethodInfo.java | 55 ++ .../interpreter/type/ScriptTypeInfo.java | 201 +++++++ .../customnpcs/textures/gui/script/icons.png | Bin 0 -> 1388 bytes 7 files changed, 990 insertions(+), 27 deletions(-) create mode 100644 src/main/resources/assets/customnpcs/textures/gui/script/icons.png 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 cb884ac96..74eb1a5c7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -4,6 +4,7 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; +import net.minecraft.util.ResourceLocation; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.script.GuiScriptInterface; @@ -13,6 +14,7 @@ // New interpreter system imports import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; @@ -95,6 +97,17 @@ private int getPaddedLineCount() { private ITextChangeListener listener; private static int LINE_NUMBER_GUTTER_WIDTH = 25; + // ==================== GUTTER ICONS ==================== + /** Icon width for override/implements icons in the gutter (scaled from 32x32 to fit line height) */ + private static final int GUTTER_ICON_SIZE = 10; + /** Extra gutter width to accommodate inheritance icons */ + private static final int ICON_GUTTER_WIDTH = 12; + /** Texture resource for script icons (64x32: first 32x32 = override, second 32x32 = implements) */ + private static final ResourceLocation SCRIPT_ICONS = new ResourceLocation("customnpcs", "textures/gui/script/icons.png"); + /** Hover state for gutter icons - tracks which icon the mouse is over */ + private MethodInfo hoveredGutterMethod = null; + private int hoveredGutterLine = -1; + // ==================== UNDO/REDO ==================== public List undoList = new ArrayList<>(); public List redoList = new ArrayList<>(); @@ -465,12 +478,12 @@ public void drawTextBox(int xMouse, int yMouse) { return; clampSelectionBounds(); - // Dynamically calculate gutter width based on line count digits + // Dynamically calculate gutter width based on line count digits + icon space if (container != null && container.linesCount > 0) { int maxLineNum = container.linesCount; String maxLineStr = String.valueOf(maxLineNum); int digitWidth = ClientProxy.Font.width(maxLineStr); - LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right) + LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10 + ICON_GUTTER_WIDTH; // 10px padding + icon space } // Draw outer border around entire area int offset = fullscreen() ? 2 : 1; @@ -638,7 +651,7 @@ public void drawTextBox(int xMouse, int yMouse) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; String lineNum = String.valueOf(i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); - int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding + int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5 - ICON_GUTTER_WIDTH; // right-align before icon space int lineNumY = posY + 1; // Highlight current line number int lineNumColor = 0xFF606366; @@ -655,7 +668,7 @@ public void drawTextBox(int xMouse, int yMouse) { } ClientProxy.Font.drawString(lineNum, lineNumX, lineNumY, lineNumColor); } - + // Render Viewport for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); @@ -864,6 +877,12 @@ public void drawTextBox(int xMouse, int yMouse) { } } } + + // Render GUTTER ICONS for method override/implements + hoveredGutterMethod = null; + hoveredGutterLine = -1; + renderGutterIcons(renderStart, renderEnd, stringYOffset, xMouse, yMouse, fracPixels); + GL11.glPopMatrix(); GL11.glDisable(GL11.GL_SCISSOR_TEST); @@ -907,6 +926,128 @@ private void scissorViewport() { int scissorH = this.height * scaleFactor; GL11.glScissor(scissorX, scissorY, scissorW, scissorH); } + + // ==================== GUTTER ICON RENDERING ==================== + + /** + * Render override/implements icons in the gutter for methods with inheritance markers. + * Icons are rendered at the start of lines where method declarations with inheritance exist. + */ + private void renderGutterIcons(int renderStart, int renderEnd, int stringYOffset, int xMouse, int yMouse, float fracPixels) { + if (container == null || container.getDocument() == null) return; + + // Icon position in the gutter (right side of gutter, before line numbers) + int iconX = x + LINE_NUMBER_GUTTER_WIDTH - ICON_GUTTER_WIDTH + 1; + + // Adjust mouse Y for fractional scroll + float adjustedMouseY = yMouse + fracPixels; + + for (int lineIndex = renderStart; lineIndex <= renderEnd; lineIndex++) { + // Get method info for this line if it has an inheritance marker + MethodInfo method = getMethodWithInheritanceMarkerAtLine(lineIndex); + if (method == null) continue; + + int posY = y + (lineIndex - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; + int iconY = posY + (container.lineHeight - GUTTER_ICON_SIZE) / 2 - 1; + + // Determine which icon to draw (override = 0, implements = 1) + boolean isOverride = method.isOverride(); + int iconU = isOverride ? 0 : 32; // U offset in texture (0 for override, 32 for implements) + + // Bind the icons texture + Minecraft.getMinecraft().renderEngine.bindTexture(SCRIPT_ICONS); + + // Draw the icon scaled from 32x32 to GUTTER_ICON_SIZE + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glPushMatrix(); + float scale = 2; + GL11.glScalef(scale,scale,scale); + GL11.glTranslatef(-4,-3.25F,0); + GuiUtil.drawScaledTexturedRect((int) (iconX/scale), (int) (iconY/scale), iconU, 0, 32, 32, GUTTER_ICON_SIZE, GUTTER_ICON_SIZE, 64, 32); + GL11.glPopMatrix(); + // Check if mouse is hovering over this icon (for tooltip) + int screenPosY = y + (lineIndex - scroll.getScrolledLine()) * container.lineHeight; + if (xMouse >= iconX && xMouse < iconX + GUTTER_ICON_SIZE && + adjustedMouseY >= screenPosY && adjustedMouseY < screenPosY + container.lineHeight) { + hoveredGutterMethod = method; + hoveredGutterLine = lineIndex; + } + } + + // Render gutter icon tooltip if hovering + renderGutterIconTooltip(xMouse, yMouse); + } + + /** + * Get a method with an inheritance marker (override or implements) that starts on the given line. + */ + private MethodInfo getMethodWithInheritanceMarkerAtLine(int lineIndex) { + if (container == null || container.getDocument() == null) return null; + if (lineIndex < 0 || lineIndex >= container.lines.size()) return null; + + LineData lineData = container.lines.get(lineIndex); + int lineStart = lineData.start; + int lineEnd = lineData.end; + + // Check all methods in the document + for (MethodInfo method : container.getDocument().getAllMethods()) { + if (!method.hasInheritanceMarker()) continue; + + // Check if the method's name offset is on this line + int nameOffset = method.getNameOffset(); + if (nameOffset >= lineStart && nameOffset < lineEnd) { + return method; + } + } + + return null; + } + + /** + * Render tooltip for the currently hovered gutter icon. + */ + private void renderGutterIconTooltip(int xMouse, int yMouse) { + if (hoveredGutterMethod == null) return; + + // Build tooltip text + String tooltipText; + if (hoveredGutterMethod.isOverride()) { + String parentName = hoveredGutterMethod.getOverridesFrom() != null + ? hoveredGutterMethod.getOverridesFrom().getSimpleName() + : "parent class"; + tooltipText = "Overrides method in " + parentName; + } else { + String ifaceName = hoveredGutterMethod.getImplementsFrom() != null + ? hoveredGutterMethod.getImplementsFrom().getSimpleName() + : "interface"; + tooltipText = "Implements method from " + ifaceName; + } + + // Calculate tooltip position (to the right of the icon) + int tooltipX = xMouse + 10; + int tooltipY = yMouse - 5; + + // Measure text width + int textWidth = ClientProxy.Font.width(tooltipText); + int padding = 4; + + // Draw tooltip background + drawRect(tooltipX - padding, tooltipY - padding, + tooltipX + textWidth + padding, tooltipY + ClientProxy.Font.height() + padding, + 0xE0000000); + + // Draw tooltip border + drawRect(tooltipX - padding, tooltipY - padding, + tooltipX + textWidth + padding, tooltipY - padding + 1, + 0xFF505050); + drawRect(tooltipX - padding, tooltipY + ClientProxy.Font.height() + padding - 1, + tooltipX + textWidth + padding, tooltipY + ClientProxy.Font.height() + padding, + 0xFF505050); + + // Draw tooltip text + ClientProxy.Font.drawString(tooltipText, tooltipX, tooltipY, 0xFFFFFFFF); + } + // ==================== SELECTION & CURSOR POSITION ==================== // Get cursor position from mouse coordinates diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java index 9a9fc71ca..d257be4f7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java @@ -159,6 +159,32 @@ public static void drawTexturedModalRect(double x, double y, double width, doubl tessellator.draw(); } + /** + * Draw a scaled textured rectangle (for rendering icons). + */ + public static void drawScaledTexturedRect(int x, int y, int u, int v, int srcWidth, int srcHeight, + int destWidth, int destHeight, int textureWidth, int textureHeight) { + float uScale = 1.0f / textureWidth; + float vScale = 1.0f / textureHeight; + + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + GL11.glBegin(GL11.GL_QUADS); + GL11.glTexCoord2f(u * uScale, v * vScale); + GL11.glVertex2f(x, y); + GL11.glTexCoord2f(u * uScale, (v + srcHeight) * vScale); + GL11.glVertex2f(x, y + destHeight); + GL11.glTexCoord2f((u + srcWidth) * uScale, (v + srcHeight) * vScale); + GL11.glVertex2f(x + destWidth, y + destHeight); + GL11.glTexCoord2f((u + srcWidth) * uScale, v * vScale); + GL11.glVertex2f(x + destWidth, y); + GL11.glEnd(); + + GL11.glDisable(GL11.GL_BLEND); + } + + public static void setMouse(int guiX, int guiY) { Minecraft mc = Minecraft.getMinecraft(); ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); 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 9227c745e..e63e1cbb3 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 @@ -349,6 +349,9 @@ private void parseStructure() { // Parse and validate assignments (reassignments) - stores in FieldInfo parseAssignments(); + + // Detect method overrides and interface implementations for script types + detectMethodInheritance(); } /** @@ -356,11 +359,12 @@ private void parseStructure() { * Creates ScriptTypeInfo instances and stores them for later resolution. */ private void parseScriptTypes() { - // Pattern: [modifiers] (class|interface|enum) ClassName [optional ()] { ... } + // Pattern: [modifiers] (class|interface|enum) ClassName [optional ()] [extends Parent] [implements I1, I2...] { ... } // Matches optional modifiers (public, private, static, final, abstract) before class/interface/enum // Also allows optional () after the class name (common mistake) + // Groups: 1=class|interface|enum, 2=TypeName, 3=extends clause (optional), 4=implements clause (optional) Pattern typeDecl = Pattern.compile( - "(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:\\(\\))?\\s*(?:extends\\s+[A-Za-z_][a-zA-Z0-9_.]*)?\\s*(?:implements\\s+[A-Za-z_][a-zA-Z0-9_.,\\s]*)?\\s*\\{"); + "(?:(?:public|private|protected|static|final|abstract)\\s+)*(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:\\(\\))?\\s*(?:extends\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*(?:implements\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*\\{"); Matcher m = typeDecl.matcher(text); while (m.find()) { @@ -369,6 +373,9 @@ private void parseScriptTypes() { String kindStr = m.group(1); String typeName = m.group(2); + String extendsClause = m.group(3); // e.g., "ParentClass" or null + String implementsClause = m.group(4); // e.g., "Interface1, Interface2" or null + int bodyStart = text.indexOf('{', m.start()); int bodyEnd = findMatchingBrace(bodyStart); @@ -390,8 +397,37 @@ private void parseScriptTypes() { ScriptTypeInfo scriptType = ScriptTypeInfo.create( typeName, kind, m.start(), bodyStart, bodyEnd, modifiers); + // Store the script type first so it can be resolved by other types scriptTypes.put(typeName, scriptType); + // Process extends clause (parent class) + if (extendsClause != null && !extendsClause.trim().isEmpty()) { + String parentName = extendsClause.trim(); + TypeInfo parentType = resolveType(parentName); + if (parentType == null) { + // Create unresolved type info for display purposes + parentType = TypeInfo.unresolved(parentName, parentName); + } + scriptType.setSuperClass(parentType, parentName); + } + + // Process implements clause (interfaces) + if (implementsClause != null && !implementsClause.trim().isEmpty()) { + String[] interfaceNames = implementsClause.split(","); + for (String ifaceName : interfaceNames) { + String trimmedName = ifaceName.trim(); + if (trimmedName.isEmpty()) + continue; + + TypeInfo ifaceType = resolveType(trimmedName); + if (ifaceType == null) { + // Create unresolved type info for display purposes + ifaceType = TypeInfo.unresolved(trimmedName, trimmedName); + } + scriptType.addImplementedInterface(ifaceType, trimmedName); + } + } + // Parse fields and methods inside this type AFTER adding the scriptType globally, so its members can reference it parseScriptTypeMembers(scriptType); @@ -628,25 +664,25 @@ private int findFullDeclarationStart(int typeStart, String sourceText) { * Check for duplicate method declarations with same signature in same scope. */ private void checkDuplicateMethods() { - // Group methods by scope depth - Map> methodsByScope = new HashMap<>(); + // Group methods by their containing type (script class/interface or document root) + // Methods are only duplicates if they're in the SAME containing type + Map> methodsByType = new HashMap<>(); for (MethodInfo method : getAllMethods()) { - int scopeDepth = calculateScopeDepth(method.getFullDeclarationOffset()); - methodsByScope.computeIfAbsent(scopeDepth, k -> new ArrayList<>()).add(method); + TypeInfo containingType = method.getContainingType(); + methodsByType.computeIfAbsent(containingType, k -> new ArrayList<>()).add(method); } - - - // Check each scope for duplicates - for (List scopeMethods : methodsByScope.values()) { + + // Check each type for duplicate method signatures + for (List typeMethods : methodsByType.values()) { Map> signatureMap = new HashMap<>(); - - for (MethodInfo method : scopeMethods) { + + for (MethodInfo method : typeMethods) { MethodSignature signature = method.getSignature(); signatureMap.computeIfAbsent(signature, k -> new ArrayList<>()).add(method); } - - // Mark all methods with duplicate signatures + + // Mark all methods with duplicate signatures within the same type for (Map.Entry> entry : signatureMap.entrySet()) { List duplicates = entry.getValue(); if (duplicates.size() > 1) { @@ -1622,7 +1658,14 @@ private void markImports(List marks) { } private void markClassDeclarations(List marks) { - Matcher m = CLASS_DECL_PATTERN.matcher(text); + // Extended pattern to capture extends and implements clauses + // Groups: 1=class|interface|enum, 2=TypeName, 3=extends clause, 4=implements clause + Pattern classWithInheritance = Pattern.compile( + "\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)\\s*(?:\\(\\))?\\s*" + + "(?:(extends)\\s+([A-Za-z_][a-zA-Z0-9_.]*))?\\s*" + + "(?:(implements)\\s+([A-Za-z_][a-zA-Z0-9_.,\\s]*))?\\s*(?=\\{)"); + + Matcher m = classWithInheritance.matcher(text); while (m.find()) { if (isExcluded(m.start())) continue; @@ -1640,7 +1683,100 @@ private void markClassDeclarations(List marks) { } else { nameType = TokenType.CLASS_DECL; } - marks.add(new ScriptLine.Mark(m.start(2), m.end(2), nameType)); + String typeName = m.group(2); + marks.add(new ScriptLine.Mark(m.start(2), m.end(2), nameType, scriptTypes.get(typeName))); + + // Mark extends clause + if (m.group(3) != null) { + //Extends key word + marks.add(new ScriptLine.Mark(m.start(3), m.end(3), TokenType.IMPORT_KEYWORD)); + markExtendsClause(marks, m.start(4), m.group(4)); + } + + // Mark implements clause + if (m.group(5) != null) { + //Implements key word + marks.add(new ScriptLine.Mark(m.start(5),m.end(5), TokenType.IMPORT_KEYWORD)); + markImplementsClause(marks, m.start(6), m.group(6)); + } + } + } + + /** + * Mark the extends clause with proper coloring. + * The parent class is colored based on its resolved type. + */ + private void markExtendsClause(List marks, int clauseStart, String parentName) { + String trimmedName = parentName.trim(); + if (trimmedName.isEmpty()) + return; + + + // Resolve the parent type + TypeInfo parentType = resolveType(trimmedName); + TokenType tokenType; + + if (parentType != null && parentType.isResolved()) { + // Use the type's specific token type (CLASS, INTERFACE, ENUM) + tokenType = parentType.getTokenType(); + } else { + // Unresolved - mark as undefined + tokenType = TokenType.UNDEFINED_VAR; + if (parentType == null) { + parentType = TypeInfo.unresolved(trimmedName, trimmedName); + } + } + + // Find actual position in text (might have leading whitespace in the group) + int actualStart = clauseStart; + String fullClause = parentName; + while (actualStart < clauseStart + fullClause.length() && + Character.isWhitespace(text.charAt(actualStart))) { + actualStart++; + } + + marks.add(new ScriptLine.Mark(actualStart, actualStart + trimmedName.length(), tokenType, parentType)); + } + + /** + * Mark the implements clause with proper coloring. + * Each interface is colored based on its resolved type. + */ + private void markImplementsClause(List marks, int clauseStart, String implementsList) { + String[] interfaces = implementsList.split(","); + int currentPos = clauseStart; + + for (String ifaceName : interfaces) { + String trimmedName = ifaceName.trim(); + if (trimmedName.isEmpty()) { + currentPos += ifaceName.length() + 1; // +1 for comma + continue; + } + + // Find the actual start position of this interface name in the text + int leadingSpaces = 0; + while (leadingSpaces < ifaceName.length() && Character.isWhitespace(ifaceName.charAt(leadingSpaces))) { + leadingSpaces++; + } + int actualStart = currentPos + leadingSpaces; + + // Resolve the interface type + TypeInfo ifaceType = resolveType(trimmedName); + TokenType tokenType; + + if (ifaceType != null && ifaceType.isResolved()) { + tokenType = ifaceType.getTokenType(); + } else { + tokenType = TokenType.UNDEFINED_VAR; + if (ifaceType == null) { + ifaceType = TypeInfo.unresolved(trimmedName, trimmedName); + } + } + + marks.add(new ScriptLine.Mark(actualStart, actualStart + trimmedName.length(), tokenType, ifaceType)); + + // Move past this interface name and the comma + currentPos += ifaceName.length() + 1; } } @@ -4064,6 +4200,352 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st // Attach as the declaration assignment targetField.setDeclarationAssignment(info); } + + // ==================== METHOD INHERITANCE DETECTION ==================== + + /** + * Detect method overrides and interface implementations for all script-defined types. + * This analyzes each method in each ScriptTypeInfo to determine if it: + * - Overrides a method from a parent class (extends) + * - Implements a method from an interface (implements) + * + * The detection uses signature matching (method name + parameter types). + * Also validates that all interface methods are implemented and constructors match. + */ + private void detectMethodInheritance() { + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + scriptType.clearErrors(); // Clear previous errors + detectMethodInheritanceForType(scriptType); + validateScriptType(scriptType); // Validate after detecting inheritance + } + } + + /** + * Detect method inheritance for a single script type. + */ + private void detectMethodInheritanceForType(ScriptTypeInfo scriptType) { + // Check each method in this type + for (List overloads : scriptType.getMethods().values()) { + for (MethodInfo method : overloads) { + // Check if method overrides a parent class method + if (scriptType.hasSuperClass()) { + TypeInfo superClass = scriptType.getSuperClass(); + if (superClass != null && superClass.isResolved()) { + TypeInfo overrideSource = findMethodInHierarchy(superClass, method, false); + if (overrideSource != null) { + method.setOverridesFrom(overrideSource); + } + } + } + + // Check if method implements an interface method (only if not already marked as override) + if (!method.isOverride() && scriptType.hasImplementedInterfaces()) { + for (TypeInfo iface : scriptType.getImplementedInterfaces()) { + if (iface != null && iface.isResolved()) { + TypeInfo implementsSource = findMethodInInterface(iface, method); + if (implementsSource != null) { + method.setImplementsFrom(implementsSource); + break; // Only mark first matching interface + } + } + } + } + } + } + } + + /** + * Validate a script type for missing interface methods and constructor matching. + */ + private void validateScriptType(ScriptTypeInfo scriptType) { + // Skip validation for interfaces - they don't implement methods + if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) + return; + + // Check that all interface methods are implemented + if (scriptType.hasImplementedInterfaces()) { + for (TypeInfo iface : scriptType.getImplementedInterfaces()) { + if (iface == null || !iface.isResolved()) { + // Mark unresolved interface error + scriptType.setError(ScriptTypeInfo.ErrorType.UNRESOLVED_INTERFACE, + "Cannot resolve interface"); + continue; + } + validateInterfaceImplementation(scriptType, iface); + } + } + + // Check that extending class has a matching constructor + if (scriptType.hasSuperClass()) { + TypeInfo superClass = scriptType.getSuperClass(); + if (superClass == null || !superClass.isResolved()) { + scriptType.setError(ScriptTypeInfo.ErrorType.UNRESOLVED_PARENT, + "Cannot resolve parent class " + scriptType.getSuperClassName()); + } else { + validateConstructorChain(scriptType, superClass); + } + } + } + + /** + * Validate that a script type implements all methods from an interface. + */ + private void validateInterfaceImplementation(ScriptTypeInfo scriptType, TypeInfo iface) { + // Get all methods from the interface + Class javaClass = iface.getJavaClass(); + if (javaClass == null || !javaClass.isInterface()) + return; + + try { + for (java.lang.reflect.Method javaMethod : javaClass.getMethods()) { + // Skip static and default methods + if (java.lang.reflect.Modifier.isStatic(javaMethod.getModifiers())) + continue; + + // Check if scriptType has a matching method + String methodName = javaMethod.getName(); + int paramCount = javaMethod.getParameterCount(); + + boolean found = false; + List overloads = scriptType.getAllMethodOverloads(methodName); + for (MethodInfo method : overloads) { + if (parameterTypesMatch(method, javaMethod)) { + found = true; + break; + } + } + + if (!found) { + // Build signature string for error message + StringBuilder sig = new StringBuilder("("); + Class[] paramTypes = javaMethod.getParameterTypes(); + for (int i = 0; i < paramTypes.length; i++) { + if (i > 0) + sig.append(", "); + sig.append(paramTypes[i].getSimpleName()); + } + sig.append(")"); + + scriptType.addMissingMethodError(iface, methodName, sig.toString()); + } + } + } catch (Exception e) { + // Security or linkage error + } + } + + /** + * Validate that a script type has a constructor compatible with its parent class. + * This checks that for each parent constructor, there's a matching constructor in the child. + */ + private void validateConstructorChain(ScriptTypeInfo scriptType, TypeInfo superClass) { + // If no constructors defined in script type, it has an implicit default constructor + // Check if parent has a no-arg constructor + if (!scriptType.hasConstructors()) { + boolean parentHasNoArg = false; + + if (superClass instanceof ScriptTypeInfo) { + ScriptTypeInfo parentScript = (ScriptTypeInfo) superClass; + if (!parentScript.hasConstructors() || parentScript.findConstructor(0) != null) { + parentHasNoArg = true; + } + } else { + Class javaClass = superClass.getJavaClass(); + if (javaClass != null) { + try { + for (java.lang.reflect.Constructor ctor : javaClass.getConstructors()) { + if (ctor.getParameterCount() == 0) { + parentHasNoArg = true; + break; + } + } + } catch (Exception e) { + // Security error + } + } + } + + if (!parentHasNoArg) { + scriptType.addConstructorMismatchError(superClass, + superClass.getSimpleName() + "()"); + } + } + // If script type has constructors, we'd need to check super() calls - that's more complex + // For now, we just validate the implicit default constructor case + } + + /** + * Search for a matching method in a type hierarchy (class inheritance). + * @param type The type to search in (and its superclasses) + * @param method The method to find a match for + * @param includeInterfaces Whether to also search interfaces + * @return The TypeInfo containing the matching method, or null if not found + */ + private TypeInfo findMethodInHierarchy(TypeInfo type, MethodInfo method, boolean includeInterfaces) { + if (type == null || !type.isResolved()) + return null; + + // Check if this type has the method + if (hasMatchingMethod(type, method)) { + return type; + } + + // If it's a ScriptTypeInfo, check its super class + if (type instanceof ScriptTypeInfo) { + ScriptTypeInfo scriptType = (ScriptTypeInfo) type; + if (scriptType.hasSuperClass()) { + TypeInfo result = findMethodInHierarchy(scriptType.getSuperClass(), method, includeInterfaces); + if (result != null) + return result; + } + } + + // If it's a Java class, check its superclass + Class javaClass = type.getJavaClass(); + if (javaClass != null) { + Class superClass = javaClass.getSuperclass(); + if (superClass != null && superClass != Object.class) { + TypeInfo superType = TypeInfo.fromClass(superClass); + TypeInfo result = findMethodInHierarchy(superType, method, includeInterfaces); + if (result != null) + return result; + } + } + + return null; + } + + /** + * Search for a matching method in an interface and its super-interfaces. + * @param iface The interface to search in + * @param method The method to find a match for + * @return The TypeInfo containing the matching method, or null if not found + */ + private TypeInfo findMethodInInterface(TypeInfo iface, MethodInfo method) { + if (iface == null || !iface.isResolved()) + return null; + + // Check if this interface has the method + if (hasMatchingMethod(iface, method)) { + return iface; + } + + // Check super-interfaces + Class javaClass = iface.getJavaClass(); + if (javaClass != null && javaClass.isInterface()) { + for (Class superIface : javaClass.getInterfaces()) { + TypeInfo superType = TypeInfo.fromClass(superIface); + TypeInfo result = findMethodInInterface(superType, method); + if (result != null) + return result; + } + } + + return null; + } + + /** + * Check if a type has a method matching the given signature. + * Uses method name and parameter types for matching. + */ + private boolean hasMatchingMethod(TypeInfo type, MethodInfo method) { + String methodName = method.getName(); + int paramCount = method.getParameterCount(); + + // For ScriptTypeInfo, check its methods map + if (type instanceof ScriptTypeInfo) { + ScriptTypeInfo scriptType = (ScriptTypeInfo) type; + List overloads = scriptType.getAllMethodOverloads(methodName); + for (MethodInfo candidate : overloads) { + if (signaturesMatch(method, candidate)) { + return true; + } + } + return false; + } + + // For Java classes, use reflection + Class javaClass = type.getJavaClass(); + if (javaClass == null) + return false; + + try { + for (java.lang.reflect.Method javaMethod : javaClass.getMethods()) { + if (javaMethod.getName().equals(methodName) && + javaMethod.getParameterCount() == paramCount) { + // Check parameter types match + if (parameterTypesMatch(method, javaMethod)) { + return true; + } + } + } + } catch (Exception e) { + // Security or linkage error + } + + return false; + } + + /** + * Check if two MethodInfo signatures match (same name and parameter types). + */ + private boolean signaturesMatch(MethodInfo m1, MethodInfo m2) { + if (!m1.getName().equals(m2.getName())) + return false; + if (m1.getParameterCount() != m2.getParameterCount()) + return false; + + List params1 = m1.getParameters(); + List params2 = m2.getParameters(); + + for (int i = 0; i < params1.size(); i++) { + TypeInfo type1 = params1.get(i).getDeclaredType(); + TypeInfo type2 = params2.get(i).getDeclaredType(); + + // Both null = match + if (type1 == null && type2 == null) + continue; + + // One null, one not = no match + if (type1 == null || type2 == null) + return false; + + // Compare full names + if (!type1.getFullName().equals(type2.getFullName())) { + return false; + } + } + + return true; + } + + /** + * Check if a MethodInfo's parameter types match a Java reflection Method's parameter types. + */ + private boolean parameterTypesMatch(MethodInfo methodInfo, java.lang.reflect.Method javaMethod) { + List params = methodInfo.getParameters(); + Class[] javaParams = javaMethod.getParameterTypes(); + + if (params.size() != javaParams.length) + return false; + + for (int i = 0; i < params.size(); i++) { + TypeInfo paramType = params.get(i).getDeclaredType(); + if (paramType == null) + continue; // Unresolved param, skip check + + Class javaParamClass = javaParams[i]; + String javaParamName = javaParamClass.getName(); + + // Compare type names + if (!paramType.getFullName().equals(javaParamName) && + !paramType.getSimpleName().equals(javaParamClass.getSimpleName())) { + return false; + } + } + + return true; + } /** * Mark types in cast expressions with their appropriate type color. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 02e65fe1a..a19ad0020 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -495,6 +495,65 @@ private void extractClassInfo(Token token) { : TokenType.IMPORTED_CLASS.getHexColor(); addSegment(typeInfo.getSimpleName(), classColor); + // Extends clause for ScriptTypeInfo + if (scriptType.hasSuperClass()) { + addSegment(" extends ", TokenType.MODIFIER.getHexColor()); + TypeInfo superClass = scriptType.getSuperClass(); + if (superClass != null && superClass.isResolved()) { + // Color based on resolved type + addSegment(superClass.getSimpleName(), getColorForTypeInfo(superClass)); + } else { + // Unresolved - show the raw name in undefined color + String superName = scriptType.getSuperClassName(); + if (superName != null) { + addSegment(superName, TokenType.UNDEFINED_VAR.getHexColor()); + } + } + } + + // Implements clause for ScriptTypeInfo + List implementedInterfaces = scriptType.getImplementedInterfaces(); + if (!implementedInterfaces.isEmpty()) { + // For interfaces, they "extend" other interfaces; for classes, they "implement" + String keyword = scriptType.getKind() == TypeInfo.Kind.INTERFACE ? " extends " : " implements "; + if (scriptType.hasSuperClass() && scriptType.getKind() == TypeInfo.Kind.INTERFACE) { + // Already showed extends, so use comma + addSegment(", ", TokenType.DEFAULT.getHexColor()); + } else { + addSegment(keyword, TokenType.MODIFIER.getHexColor()); + } + + List interfaceNames = scriptType.getImplementedInterfaceNames(); + for (int i = 0; i < implementedInterfaces.size(); i++) { + if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); + + TypeInfo ifaceType = implementedInterfaces.get(i); + String ifaceName = (i < interfaceNames.size()) ? interfaceNames.get(i) : ifaceType.getSimpleName(); + + if (ifaceType != null && ifaceType.isResolved()) { + addSegment(ifaceName, getColorForTypeInfo(ifaceType)); + } else { + addSegment(ifaceName, TokenType.UNDEFINED_VAR.getHexColor()); + } + } + } + + // Add ScriptTypeInfo errors + if (scriptType.hasError()) { + // Missing interface method errors + for (ScriptTypeInfo.MissingMethodError err : scriptType.getMissingMethodErrors()) { + errors.add(err.getMessage()); + } + // Constructor mismatch errors + for (ScriptTypeInfo.ConstructorMismatchError err : scriptType.getConstructorMismatchErrors()) { + errors.add(err.getMessage()); + } + // General error message + if (scriptType.getErrorMessage() != null) { + errors.add(scriptType.getErrorMessage()); + } + } + } else { // Unresolved type iconIndicator = "?"; @@ -939,15 +998,14 @@ private int getColorForClass(Class clazz) { } /** - * Get the appropriate color for a TypeInfo based on its Java class. + * Get the appropriate color for a TypeInfo based on its type kind. + * Works for both Java-backed TypeInfo and ScriptTypeInfo. */ private int getColorForTypeInfo(TypeInfo typeInfo) { - if (typeInfo != null) { - Class clazz = typeInfo.getJavaClass(); - if (clazz != null) - return getColorForClass(clazz); - } - return TokenType.IMPORTED_CLASS.getHexColor(); + if (typeInfo == null) return TokenType.IMPORTED_CLASS.getHexColor(); + + // Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly + return typeInfo.getTokenType().getHexColor(); } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 5b64373bf..21a4cdc71 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -63,6 +63,20 @@ public enum ErrorType { private String errorMessage; private List parameterErrors = new ArrayList<>(); private List returnStatementErrors = new ArrayList<>(); + + // ==================== OVERRIDE/IMPLEMENTS TRACKING ==================== + + /** + * The type containing the method that this method overrides (the super class). + * Null if this method doesn't override anything. + */ + private TypeInfo overridesFrom; + + /** + * The type containing the interface method that this method implements. + * Null if this method doesn't implement an interface method. + */ + private TypeInfo implementsFrom; private MethodInfo(String name, TypeInfo returnType, TypeInfo containingType, List parameters, int fullDeclarationOffset, int typeOffset, int nameOffset, @@ -174,6 +188,47 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T public boolean isPrivate() { return Modifier.isPrivate(modifiers); } public boolean isProtected() { return Modifier.isProtected(modifiers); } public String getDocumentation() { return documentation; } + + // ==================== OVERRIDE/IMPLEMENTS GETTERS/SETTERS ==================== + + /** + * Check if this method overrides a parent class method. + */ + public boolean isOverride() { return overridesFrom != null; } + + /** + * Get the type containing the method this overrides. + * @return The parent class TypeInfo, or null if not an override + */ + public TypeInfo getOverridesFrom() { return overridesFrom; } + + /** + * Mark this method as overriding a parent class method. + * @param parentType The type containing the overridden method + */ + public void setOverridesFrom(TypeInfo parentType) { this.overridesFrom = parentType; } + + /** + * Check if this method implements an interface method. + */ + public boolean isImplements() { return implementsFrom != null; } + + /** + * Get the interface containing the method this implements. + * @return The interface TypeInfo, or null if not implementing + */ + public TypeInfo getImplementsFrom() { return implementsFrom; } + + /** + * Mark this method as implementing an interface method. + * @param interfaceType The interface containing the implemented method + */ + public void setImplementsFrom(TypeInfo interfaceType) { this.implementsFrom = interfaceType; } + + /** + * Check if this method either overrides or implements something. + */ + public boolean hasInheritanceMarker() { return isOverride() || isImplements(); } /** * Check if a position is inside this method's body. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index e3b405b1a..0f84772f5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -33,6 +33,33 @@ public class ScriptTypeInfo extends TypeInfo { // Parent class reference (for inner class resolution) private ScriptTypeInfo outerClass; + // ==================== INHERITANCE ==================== + + /** + * The parent/super class for this type (from "extends ParentClass"). + * Can be a resolved TypeInfo or ScriptTypeInfo, or unresolved TypeInfo if the parent is not found. + * Null if this type doesn't extend anything (or extends Object implicitly). + */ + private TypeInfo superClass; + + /** + * The raw string name of the super class as written in the script (e.g., "ParentClass"). + * Stored for display purposes even when the type couldn't be resolved. + */ + private String superClassName; + + /** + * All implemented interfaces (from "implements Interface1, Interface2, ..."). + * Each can be resolved or unresolved. The list order matches the declaration order. + */ + private final List implementedInterfaces = new ArrayList<>(); + + /** + * The raw string names of implemented interfaces as written in the script. + * Stored for display purposes even when types couldn't be resolved. + */ + private final List implementedInterfaceNames = new ArrayList<>(); + private ScriptTypeInfo(String simpleName, String fullName, Kind kind, int declarationOffset, int bodyStart, int bodyEnd, int modifiers) { super(simpleName, fullName, "", kind, null, true, null, true); @@ -67,6 +94,79 @@ public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTyp public ScriptTypeInfo getOuterClass() { return outerClass; } public List getInnerClasses() { return innerClasses; } + // ==================== INHERITANCE ==================== + + /** + * Get the super class (from "extends"). Can be resolved or unresolved. + * @return The parent class TypeInfo, or null if no extends clause + */ + public TypeInfo getSuperClass() { return superClass; } + + /** + * Get the raw super class name as written in the script. + * @return The super class name string, or null if no extends clause + */ + public String getSuperClassName() { return superClassName; } + + /** + * Set the super class info. Call this after resolving types. + * @param superClass The resolved or unresolved TypeInfo for the parent class + * @param superClassName The raw class name as written in the script + */ + public void setSuperClass(TypeInfo superClass, String superClassName) { + this.superClass = superClass; + this.superClassName = superClassName; + } + + /** + * Check if this type has a super class (extends something). + */ + public boolean hasSuperClass() { return superClass != null || superClassName != null; } + + /** + * Get all implemented interfaces. Each can be resolved or unresolved. + * @return Unmodifiable list of implemented interface TypeInfos + */ + public List getImplementedInterfaces() { + return new ArrayList<>(implementedInterfaces); + } + + /** + * Get the raw interface names as written in the script. + * @return Unmodifiable list of interface name strings + */ + public List getImplementedInterfaceNames() { + return new ArrayList<>(implementedInterfaceNames); + } + + /** + * Add an implemented interface. Call this after resolving types. + * @param interfaceType The resolved or unresolved TypeInfo for the interface + * @param interfaceName The raw interface name as written in the script + */ + public void addImplementedInterface(TypeInfo interfaceType, String interfaceName) { + implementedInterfaces.add(interfaceType); + implementedInterfaceNames.add(interfaceName); + } + + /** + * Check if this type implements any interfaces. + */ + public boolean hasImplementedInterfaces() { return !implementedInterfaces.isEmpty(); } + + /** + * Check if this type implements a specific interface (by simple name). + */ + public boolean implementsInterface(String interfaceName) { + for (String name : implementedInterfaceNames) { + if (name.equals(interfaceName)) return true; + } + for (TypeInfo ti : implementedInterfaces) { + if (ti.getSimpleName().equals(interfaceName)) return true; + } + return false; + } + /** * Check if a position is inside this type's body. */ @@ -230,4 +330,105 @@ public String toString() { return "ScriptTypeInfo{" + scriptClassName + ", " + getKind() + ", fields=" + fields.size() + ", methods=" + methods.size() + "}"; } + + // ==================== ERROR HANDLING ==================== + + /** + * Error types for ScriptTypeInfo validation. + */ + public enum ErrorType { + NONE, + MISSING_INTERFACE_METHOD, // Class doesn't implement all interface methods + MISSING_CONSTRUCTOR_MATCH, // Class extends parent but has no matching constructor + UNRESOLVED_PARENT, // Parent class cannot be resolved + UNRESOLVED_INTERFACE // Implemented interface cannot be resolved + } + + /** + * Represents a missing interface method error. + */ + public static class MissingMethodError { + private final TypeInfo interfaceType; + private final String methodName; + private final String signature; + + public MissingMethodError(TypeInfo interfaceType, String methodName, String signature) { + this.interfaceType = interfaceType; + this.methodName = methodName; + this.signature = signature; + } + + public TypeInfo getInterfaceType() { return interfaceType; } + public String getMethodName() { return methodName; } + public String getSignature() { return signature; } + + public String getMessage() { + return "Class must implement method '" + methodName + signature + "' from interface " + interfaceType.getSimpleName(); + } + } + + /** + * Represents a constructor mismatch error. + */ + public static class ConstructorMismatchError { + private final TypeInfo parentType; + private final String parentConstructorSignature; + + public ConstructorMismatchError(TypeInfo parentType, String parentConstructorSignature) { + this.parentType = parentType; + this.parentConstructorSignature = parentConstructorSignature; + } + + public TypeInfo getParentType() { return parentType; } + public String getParentConstructorSignature() { return parentConstructorSignature; } + + public String getMessage() { + return "Class extends " + parentType.getSimpleName() + " but has no constructor matching " + parentConstructorSignature; + } + } + + // Error tracking + private ErrorType errorType = ErrorType.NONE; + private String errorMessage; + private final List missingMethodErrors = new ArrayList<>(); + private final List constructorMismatchErrors = new ArrayList<>(); + + // Error getters + public ErrorType getErrorType() { return errorType; } + public String getErrorMessage() { return errorMessage; } + public boolean hasError() { return errorType != ErrorType.NONE || !missingMethodErrors.isEmpty() || !constructorMismatchErrors.isEmpty(); } + public List getMissingMethodErrors() { return new ArrayList<>(missingMethodErrors); } + public List getConstructorMismatchErrors() { return new ArrayList<>(constructorMismatchErrors); } + + /** + * Set a general error on this type. + */ + public void setError(ErrorType type, String message) { + this.errorType = type; + this.errorMessage = message; + } + + /** + * Add a missing interface method error. + */ + public void addMissingMethodError(TypeInfo interfaceType, String methodName, String signature) { + missingMethodErrors.add(new MissingMethodError(interfaceType, methodName, signature)); + } + + /** + * Add a constructor mismatch error. + */ + public void addConstructorMismatchError(TypeInfo parentType, String parentConstructorSignature) { + constructorMismatchErrors.add(new ConstructorMismatchError(parentType, parentConstructorSignature)); + } + + /** + * Clear all errors on this type. + */ + public void clearErrors() { + errorType = ErrorType.NONE; + errorMessage = null; + missingMethodErrors.clear(); + constructorMismatchErrors.clear(); + } } diff --git a/src/main/resources/assets/customnpcs/textures/gui/script/icons.png b/src/main/resources/assets/customnpcs/textures/gui/script/icons.png new file mode 100644 index 0000000000000000000000000000000000000000..4e19870f33c0bcdf349f49faaedbcd923c588897 GIT binary patch literal 1388 zcmV-y1(W)TP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D1p!G!K~!i%?Urq9 zQ&kwp|F?Jd-u1m**KLe-D;wQF3Nv(;;D7)oBH(LK3HrsDXo7suU`U9GLP8LO5kC+U zO&}qDl7tvQ1z}EJ=CXja>c$)l5n*GaZ0p*s@4fBu+}mxwW$ezCFM5ATbI-Xw_n!Ow z&-p*kfr^TXii(Pg%KsH+?3!1W*4L9AH8r>rjp9IWFD`d>-j{hMW2M(i-m0v`)mRLN z?%aXT@1J!3fBj*LJj)2n8IVaw(C+(g;yqIEyId|*6&A|C-b7z~k`{<6JW=?ji`boWK%dRK$?~F5=x` zyjxv8F@H}YfyG86&W1wRdh+DV1aL3%kcyS>L2F-#!LB0+-~JJa=m0pu2&26YdV4K` zZM(3ctPGzA0(gg7;6gZzpGHP7&tSm5bLVCxfXCZF${(pfVTl$4?Ewt;h7cc(fh*); zDKo=RVnnz#fT!(t9HYB#JGH>2(NT0nBB<5ru(!E+W&*6)@)hCDPFz2~2kBT?zQ^nB z5aVHFvnend7Hs-1G-B?*-P8t>0DI1ynNi^5tl#z|;Ve9^HFYDMOo7w#WBfgnpcUTq7gS*_R7KA>(;@&;SlCO@e$n5eF}$n zGZlb`$QFUnYra|j)C!`nG~#;mHKbxm6q>bgKf7q+XPaw7Hj_b|NgyLBF%1+6U~xTz z;PsP8MuRAtvl6B5=W(lTKiUs(ME9xf&=tE-;dz;kWRPJkxrg$Ism_Yw?jRD8gxrq@ zWAGpP9wbX}^+Xp2y6(#NB~qI7SW6j-1dx0m>F6P2#B*Hy(@fmRX z5)kQ?G#}&>4X;5escdD50j5Hc0I`4{rt${49q2rQV%KA^&wCt# zsT^j<63lLRh1%jcO+-nU%O8Y&@fPfFX<<(Ie0iw=vHmD5j@$xBFf2oNV(y^BX*p=P zKX#mzjv$A`4~9Bpfxa(+bu40j!d*|`cjtAl#qkDIL@z*@Beb!+!wR{e_@xcVjT zbbN+_0xj$dHX?Gnb?nLf5c)X?Q?&&Kn?df6R{=)DpnBC@xe7=W@C7KTFGKX!)GI%Z z;uTu`(m`T!uECA-yAcchH3vu(2zpd6dy`rrg`1}~qw{imURHmcM_*e@jMZlR`pq>& zg8vMVrw5{%)h_B6g5DE1;rC5`{{J(Ivz1P~v6I)AqI}8g)Q={bZAAI-qchKjZO(Ey zmcBuc#C_;(-ipf|Kjbxd33Jx8lQVEAZ*)Lspp}MJAPvJpZ`Gs3RSL&)C&Y+|{?k41 zT~cnnX@6+LsuzfD{wC;VdBF-MIYB0a-3U^fTsZY{-UI(T53g+`)&(}`W*eXpG;)GW u1``N%4x#1f`TIVkq)a>;~*2S_sj0000 Date: Wed, 31 Dec 2025 19:19:06 +0200 Subject: [PATCH 125/337] Vastly improved GutterIconRenderer --- .../client/gui/util/GuiScriptTextArea.java | 153 +-------- .../interpreter/hover/GutterIconRenderer.java | 304 ++++++++++++++++++ .../script/interpreter/token/TokenType.java | 21 ++ 3 files changed, 342 insertions(+), 136 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java 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 74eb1a5c7..c45c0b817 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -17,6 +17,7 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.hover.GutterIconRenderer; import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState; import noppes.npcs.client.gui.util.script.interpreter.hover.TokenHoverRenderer; import noppes.npcs.client.key.impl.ScriptEditorKeys; @@ -98,15 +99,8 @@ private int getPaddedLineCount() { private static int LINE_NUMBER_GUTTER_WIDTH = 25; // ==================== GUTTER ICONS ==================== - /** Icon width for override/implements icons in the gutter (scaled from 32x32 to fit line height) */ - private static final int GUTTER_ICON_SIZE = 10; - /** Extra gutter width to accommodate inheritance icons */ - private static final int ICON_GUTTER_WIDTH = 12; - /** Texture resource for script icons (64x32: first 32x32 = override, second 32x32 = implements) */ - private static final ResourceLocation SCRIPT_ICONS = new ResourceLocation("customnpcs", "textures/gui/script/icons.png"); /** Hover state for gutter icons - tracks which icon the mouse is over */ private MethodInfo hoveredGutterMethod = null; - private int hoveredGutterLine = -1; // ==================== UNDO/REDO ==================== public List undoList = new ArrayList<>(); @@ -483,7 +477,7 @@ public void drawTextBox(int xMouse, int yMouse) { int maxLineNum = container.linesCount; String maxLineStr = String.valueOf(maxLineNum); int digitWidth = ClientProxy.Font.width(maxLineStr); - LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10 + ICON_GUTTER_WIDTH; // 10px padding + icon space + LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10 + GutterIconRenderer.ICON_GUTTER_WIDTH; // 10px padding + icon space } // Draw outer border around entire area int offset = fullscreen() ? 2 : 1; @@ -651,7 +645,7 @@ public void drawTextBox(int xMouse, int yMouse) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; String lineNum = String.valueOf(i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); - int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5 - ICON_GUTTER_WIDTH; // right-align before icon space + int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5 - GutterIconRenderer.ICON_GUTTER_WIDTH; // right-align before icon space int lineNumY = posY + 1; // Highlight current line number int lineNumColor = 0xFF606366; @@ -878,10 +872,13 @@ public void drawTextBox(int xMouse, int yMouse) { } } - // Render GUTTER ICONS for method override/implements - hoveredGutterMethod = null; - hoveredGutterLine = -1; - renderGutterIcons(renderStart, renderEnd, stringYOffset, xMouse, yMouse, fracPixels); + // Render gutter icons for method override/implements + if (container != null && container.getDocument() != null) { + hoveredGutterMethod = GutterIconRenderer.renderIcons(container.lineHeight, + x + LINE_NUMBER_GUTTER_WIDTH - GutterIconRenderer.ICON_GUTTER_WIDTH + 1, y, renderStart, renderEnd, + scroll.getScrolledLine(), stringYOffset, container.getDocument().getAllMethods(), container.lines, + xMouse, yMouse, fracPixels); + } GL11.glPopMatrix(); GL11.glDisable(GL11.GL_SCISSOR_TEST); @@ -905,8 +902,8 @@ public void drawTextBox(int xMouse, int yMouse) { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); - - // Draw hover tooltip (on top of everything) + + // Draw hover tooltips (on top of everything) if (hoverState.isTooltipVisible()) { int xOffset = hasVerticalScrollbar() ? -8 : -2; int viewportWidth = width - LINE_NUMBER_GUTTER_WIDTH; @@ -914,6 +911,11 @@ public void drawTextBox(int xMouse, int yMouse) { int viewportHeight = height; TokenHoverRenderer.render(hoverState, viewportX, viewportWidth+xOffset, viewportY, viewportHeight); } + + // Draw gutter icon tooltip + if (hoveredGutterMethod != null) { + GutterIconRenderer.renderTooltip(hoveredGutterMethod, xMouse, yMouse, x, width, y, height); + } } private void scissorViewport() { @@ -927,127 +929,6 @@ private void scissorViewport() { GL11.glScissor(scissorX, scissorY, scissorW, scissorH); } - // ==================== GUTTER ICON RENDERING ==================== - - /** - * Render override/implements icons in the gutter for methods with inheritance markers. - * Icons are rendered at the start of lines where method declarations with inheritance exist. - */ - private void renderGutterIcons(int renderStart, int renderEnd, int stringYOffset, int xMouse, int yMouse, float fracPixels) { - if (container == null || container.getDocument() == null) return; - - // Icon position in the gutter (right side of gutter, before line numbers) - int iconX = x + LINE_NUMBER_GUTTER_WIDTH - ICON_GUTTER_WIDTH + 1; - - // Adjust mouse Y for fractional scroll - float adjustedMouseY = yMouse + fracPixels; - - for (int lineIndex = renderStart; lineIndex <= renderEnd; lineIndex++) { - // Get method info for this line if it has an inheritance marker - MethodInfo method = getMethodWithInheritanceMarkerAtLine(lineIndex); - if (method == null) continue; - - int posY = y + (lineIndex - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; - int iconY = posY + (container.lineHeight - GUTTER_ICON_SIZE) / 2 - 1; - - // Determine which icon to draw (override = 0, implements = 1) - boolean isOverride = method.isOverride(); - int iconU = isOverride ? 0 : 32; // U offset in texture (0 for override, 32 for implements) - - // Bind the icons texture - Minecraft.getMinecraft().renderEngine.bindTexture(SCRIPT_ICONS); - - // Draw the icon scaled from 32x32 to GUTTER_ICON_SIZE - GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); - GL11.glPushMatrix(); - float scale = 2; - GL11.glScalef(scale,scale,scale); - GL11.glTranslatef(-4,-3.25F,0); - GuiUtil.drawScaledTexturedRect((int) (iconX/scale), (int) (iconY/scale), iconU, 0, 32, 32, GUTTER_ICON_SIZE, GUTTER_ICON_SIZE, 64, 32); - GL11.glPopMatrix(); - // Check if mouse is hovering over this icon (for tooltip) - int screenPosY = y + (lineIndex - scroll.getScrolledLine()) * container.lineHeight; - if (xMouse >= iconX && xMouse < iconX + GUTTER_ICON_SIZE && - adjustedMouseY >= screenPosY && adjustedMouseY < screenPosY + container.lineHeight) { - hoveredGutterMethod = method; - hoveredGutterLine = lineIndex; - } - } - - // Render gutter icon tooltip if hovering - renderGutterIconTooltip(xMouse, yMouse); - } - - /** - * Get a method with an inheritance marker (override or implements) that starts on the given line. - */ - private MethodInfo getMethodWithInheritanceMarkerAtLine(int lineIndex) { - if (container == null || container.getDocument() == null) return null; - if (lineIndex < 0 || lineIndex >= container.lines.size()) return null; - - LineData lineData = container.lines.get(lineIndex); - int lineStart = lineData.start; - int lineEnd = lineData.end; - - // Check all methods in the document - for (MethodInfo method : container.getDocument().getAllMethods()) { - if (!method.hasInheritanceMarker()) continue; - - // Check if the method's name offset is on this line - int nameOffset = method.getNameOffset(); - if (nameOffset >= lineStart && nameOffset < lineEnd) { - return method; - } - } - - return null; - } - - /** - * Render tooltip for the currently hovered gutter icon. - */ - private void renderGutterIconTooltip(int xMouse, int yMouse) { - if (hoveredGutterMethod == null) return; - - // Build tooltip text - String tooltipText; - if (hoveredGutterMethod.isOverride()) { - String parentName = hoveredGutterMethod.getOverridesFrom() != null - ? hoveredGutterMethod.getOverridesFrom().getSimpleName() - : "parent class"; - tooltipText = "Overrides method in " + parentName; - } else { - String ifaceName = hoveredGutterMethod.getImplementsFrom() != null - ? hoveredGutterMethod.getImplementsFrom().getSimpleName() - : "interface"; - tooltipText = "Implements method from " + ifaceName; - } - - // Calculate tooltip position (to the right of the icon) - int tooltipX = xMouse + 10; - int tooltipY = yMouse - 5; - - // Measure text width - int textWidth = ClientProxy.Font.width(tooltipText); - int padding = 4; - - // Draw tooltip background - drawRect(tooltipX - padding, tooltipY - padding, - tooltipX + textWidth + padding, tooltipY + ClientProxy.Font.height() + padding, - 0xE0000000); - - // Draw tooltip border - drawRect(tooltipX - padding, tooltipY - padding, - tooltipX + textWidth + padding, tooltipY - padding + 1, - 0xFF505050); - drawRect(tooltipX - padding, tooltipY + ClientProxy.Font.height() + padding - 1, - tooltipX + textWidth + padding, tooltipY + ClientProxy.Font.height() + padding, - 0xFF505050); - - // Draw tooltip text - ClientProxy.Font.drawString(tooltipText, tooltipX, tooltipY, 0xFFFFFFFF); - } - // ==================== SELECTION & CURSOR POSITION ==================== // Get cursor position from mouse coordinates diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java new file mode 100644 index 000000000..3c5087533 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/GutterIconRenderer.java @@ -0,0 +1,304 @@ +package noppes.npcs.client.gui.util.script.interpreter.hover; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renders gutter icons for method inheritance (override/implements) in the script editor. + * + * Renders IntelliJ-style tooltips when hovering over gutter icons: + * - Background + border matching TokenHoverRenderer style + * - Colored declaration showing which class/interface the method overrides/implements + * - Uses the same token highlighting system for type names + */ +public class GutterIconRenderer { + + // ==================== CONSTANTS ==================== + + /** Texture resource for script icons (64x32: first 32x32 = override, second 32x32 = implements) */ + private static final ResourceLocation SCRIPT_ICONS = new ResourceLocation("customnpcs", + "textures/gui/script/icons.png"); + + /** Icon size when rendered in the gutter (scaled from 32x32) */ + private static final int GUTTER_ICON_SIZE = 10; + + /** Extra gutter width to accommodate inheritance icons */ + public static final int ICON_GUTTER_WIDTH = 12; + + /** Padding inside the tooltip box */ + private static final int PADDING = 6; + + /** Line spacing between rows */ + private static final int LINE_SPACING = 2; + + /** Vertical offset from mouse */ + private static final int VERTICAL_OFFSET = 10; + + // ==================== COLORS ==================== + + /** Background color (dark gray like IntelliJ) */ + private static final int BG_COLOR = 0xF0313335; + + /** Border color */ + private static final int BORDER_COLOR = 0xFF3C3F41; + + /** Info text color */ + private static final int INFO_COLOR = 0xFFA9B7C6; + + // ==================== RENDERING ==================== + + /** + * Render gutter icons for a range of lines. + * + * @param lineHeight Height of each line + * @param gutterX X position for the icon in the gutter + * @param gutterY Base Y position of the gutter + * @param renderStart First line index to render + * @param renderEnd Last line index to render + * @param scrolledLine Current scroll position + * @param stringYOffset Y offset for text positioning + * @param methods All methods in the document + * @param lines Line data for position calculation + * @param xMouse Mouse X position + * @param yMouse Mouse Y position + * @param fracPixels Fractional scroll offset + * @return The hovered method, or null if no icon is hovered + */ + public static MethodInfo renderIcons( + int lineHeight, + int gutterX, + int gutterY, + int renderStart, + int renderEnd, + int scrolledLine, + int stringYOffset, + List methods, + List lines, + int xMouse, + int yMouse, + float fracPixels) { + + if (methods == null || methods.isEmpty()) + return null; + + MethodInfo hoveredMethod = null; + float adjustedMouseY = yMouse + fracPixels; + + for (int lineIndex = renderStart; lineIndex <= renderEnd; lineIndex++) { + MethodInfo method = getMethodAtLine(lineIndex, methods, lines); + if (method == null || !method.hasInheritanceMarker()) + continue; + + int posY = gutterY + (lineIndex - scrolledLine) * lineHeight + stringYOffset; + int iconY = posY + (lineHeight - GUTTER_ICON_SIZE) / 2 - 1; + + // Draw the icon + renderIcon(gutterX, iconY, method.isOverride()); + + // Check for hover + int iconScaleOffsetX = -4; + int screenPosY = gutterY + (lineIndex - scrolledLine) * lineHeight; + if (xMouse >= gutterX + iconScaleOffsetX && xMouse < gutterX + GUTTER_ICON_SIZE + iconScaleOffsetX && + adjustedMouseY >= screenPosY && adjustedMouseY < screenPosY + lineHeight) { + hoveredMethod = method; + } + } + + return hoveredMethod; + } + + /** + * Render a single gutter icon. + */ + private static void renderIcon(int x, int y, boolean isOverride) { + int iconU = isOverride ? 0 : 32; + + Minecraft.getMinecraft().renderEngine.bindTexture(SCRIPT_ICONS); + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + + GL11.glPushMatrix(); + float scale = 2.0f, scaleOffsetX = -4, scaleOffsetY = -3.25f; + GL11.glScalef(scale, scale, scale); + GL11.glTranslatef(scaleOffsetX, scaleOffsetY, 0); + GuiUtil.drawScaledTexturedRect( + (int) (x / scale), (int) (y / scale), + iconU, 0, 32, 32, + GUTTER_ICON_SIZE, GUTTER_ICON_SIZE, + 64, 32 + ); + GL11.glPopMatrix(); + } + + /** + * Render the tooltip for a hovered gutter icon. + */ + public static void renderTooltip(MethodInfo method, int mouseX, int mouseY, int viewportX, int viewportWidth, + int viewportY, int viewportHeight) { + if (method == null) + return; + + // Build tooltip content + List segments = buildTooltipContent(method); + if (segments.isEmpty()) + return; + + int lineHeight = ClientProxy.Font.height(); + + // Calculate dimensions + int contentWidth = calculateContentWidth(segments); + int contentHeight = lineHeight; + int boxWidth = contentWidth + PADDING * 2; + int boxHeight = contentHeight + PADDING * 2; + + // Position tooltip + int tooltipX = mouseX + VERTICAL_OFFSET; + int tooltipY = mouseY - 5; + + // Clamp to viewport + int rightBound = viewportX + viewportWidth; + int bottomBound = viewportY + viewportHeight; + + if (tooltipX + boxWidth > rightBound) { + tooltipX = mouseX - boxWidth - 5; + } + if (tooltipX < viewportX) { + tooltipX = viewportX; + } + + if (tooltipY + boxHeight > bottomBound) { + tooltipY = bottomBound - boxHeight; + } + if (tooltipY < viewportY) { + tooltipY = viewportY; + } + + // Render tooltip box + renderTooltipBox(tooltipX, tooltipY, boxWidth, boxHeight, segments); + } + + // ==================== PRIVATE HELPERS ==================== + + /** + * Get the method that starts on the given line. + */ + private static MethodInfo getMethodAtLine(int lineIndex, List methods, List lines) { + if (lineIndex < 0 || lineIndex >= lines.size()) + return null; + + Object lineObj = lines.get(lineIndex); + int lineStart, lineEnd; + + try { + java.lang.reflect.Field startField = lineObj.getClass().getField("start"); + java.lang.reflect.Field endField = lineObj.getClass().getField("end"); + lineStart = startField.getInt(lineObj); + lineEnd = endField.getInt(lineObj); + } catch (Exception e) { + return null; + } + + for (MethodInfo method : methods) { + if (!method.hasInheritanceMarker()) + continue; + int nameOffset = method.getNameOffset(); + if (nameOffset >= lineStart && nameOffset < lineEnd) { + return method; + } + } + + return null; + } + + /** + * Build the tooltip content as colored text segments. + */ + private static List buildTooltipContent(MethodInfo method) { + List segments = new ArrayList<>(); + + if (method.isOverride()) { + TypeInfo overridesFrom = method.getOverridesFrom(); + segments.add(new TextSegment("Overrides method in ", INFO_COLOR)); + + if (overridesFrom != null) { + int color = TokenType.getColor(overridesFrom); + segments.add(new TextSegment(overridesFrom.getSimpleName(), color)); + } else { + segments.add(new TextSegment("parent class", INFO_COLOR)); + } + } else if (method.isImplements()) { + TypeInfo implementsFrom = method.getImplementsFrom(); + segments.add(new TextSegment("Implements method from ", INFO_COLOR)); + + if (implementsFrom != null) { + int color = TokenType.getColor(implementsFrom); + segments.add(new TextSegment(implementsFrom.getSimpleName(), color)); + } else { + segments.add(new TextSegment("interface", INFO_COLOR)); + } + } + + return segments; + } + + /** + * Calculate the width needed for the segments. + */ + private static int calculateContentWidth(List segments) { + int totalWidth = 0; + for (TextSegment segment : segments) { + totalWidth += ClientProxy.Font.width(segment.text); + } + return totalWidth; + } + + /** + * Render the tooltip box with background, border, and content. + */ + private static void renderTooltipBox(int x, int y, int width, int height, List segments) { + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Draw background + int paddedHeight = y + height - 4; + Gui.drawRect(x, y, x + width, paddedHeight, BG_COLOR); + + // Draw border + Gui.drawRect(x, y, x + width, y + 1, BORDER_COLOR); // Top + Gui.drawRect(x, paddedHeight - 1, x + width, paddedHeight, BORDER_COLOR); // Bottom + Gui.drawRect(x, y, x + 1, paddedHeight, BORDER_COLOR); // Left + Gui.drawRect(x + width - 1, y, x + width, paddedHeight, BORDER_COLOR); // Right + + // Draw text segments + int currentX = x + PADDING; + int currentY = y + PADDING; + + for (TextSegment segment : segments) { + ClientProxy.Font.drawString(segment.text, currentX, currentY, 0xFF000000 | segment.color); + currentX += ClientProxy.Font.width(segment.text); + } + } + + // ==================== DATA CLASSES ==================== + + /** + * A colored text segment. + */ + private static class TextSegment { + final String text; + final int color; + + TextSegment(String text, int color) { + this.text = text; + this.color = color; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index 58368a876..b018c942f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.token; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + /** * Defines all token types for syntax highlighting with hex colors and priorities. * Priority determines which token type wins when marks overlap. @@ -57,6 +59,25 @@ public int getPriority() { return priority; } + public static int getColor(TypeInfo typeInfo) { + if (typeInfo == null) + return TokenType.IMPORTED_CLASS.getHexColor(); + + switch (typeInfo.getKind()) { + case INTERFACE: + return TokenType.INTERFACE_DECL.getHexColor(); + case ENUM: + return TokenType.ENUM_DECL.getHexColor(); + case CLASS: + return TokenType.CLASS_DECL.getHexColor(); + default: + break; + } + + // Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly + return typeInfo.getTokenType().getHexColor(); + } + /** * Convert this token type to a Minecraft color code character. * Used for backward compatibility with the existing rendering system. From 35af0bbd2d5c5177965769774a2494c0dcd21ad3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 20:42:40 +0200 Subject: [PATCH 126/337] Got error validation working for ScriptTypeInfo --- .../interpreter/ErrorUnderlineRenderer.java | 12 +++++ .../script/interpreter/ScriptDocument.java | 5 +++ .../interpreter/hover/TokenHoverInfo.java | 44 +++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java index db64cdfe7..b9778e6ab 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java @@ -5,6 +5,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; import org.lwjgl.opengl.GL11; /** @@ -151,6 +152,17 @@ else if (method.hasError()) { } } + + + for(ScriptTypeInfo types : doc.getScriptTypes()){ + if(!types.hasError()) + continue; + + int typeStart = types.getDeclarationOffset(); + int typeEnd = types.getBodyStart(); + drawUnderlineForSpan(typeStart, typeEnd, lineStartX, baselineY, + lineText, lineStart, lineEnd, ERROR_COLOR); + } } /** 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 e63e1cbb3..2842a57c0 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 @@ -22,6 +22,7 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * The main document container that manages script text, lines, and tokens. @@ -4944,6 +4945,10 @@ public List getMethods() { return Collections.unmodifiableList(methods); } + public List getScriptTypes() { + return scriptTypes.values().stream().collect(Collectors.toList()); + } + public List getAllMethods() { List allMethods = new ArrayList<>(methods); for (ScriptTypeInfo scriptType : scriptTypes.values()) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index a19ad0020..253e75086 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -135,7 +135,6 @@ public static TokenHoverInfo fromToken(Token token) { case UNDEFINED_VAR: info.extractUndefinedInfo(token); break; - case LITERAL: case KEYWORD: case MODIFIER: @@ -152,7 +151,9 @@ public static TokenHoverInfo fromToken(Token token) { info.extractMethodDeclInfo(token); } else if (token.getFieldInfo() != null) { info.extractFieldInfoGeneric(token); - } else { + } else if (info.hasErrors()) + return info; + else { return null; // Nothing to show } break; @@ -271,6 +272,22 @@ else if (methodDecl.hasError()) { } } + ScriptTypeInfo scriptType = findScriptTypeContainingPosition(token); + if (scriptType != null && scriptType.hasError()) { + // Missing interface method errors + for (ScriptTypeInfo.MissingMethodError err : scriptType.getMissingMethodErrors()) { + errors.add(err.getMessage()); + } + // Constructor mismatch errors + for (ScriptTypeInfo.ConstructorMismatchError err : scriptType.getConstructorMismatchErrors()) { + errors.add(err.getMessage()); + } + // General error message + if (scriptType.getErrorMessage() != null) { + errors.add(scriptType.getErrorMessage()); + } + } + if(token.getType() == TokenType.UNDEFINED_VAR) errors.add("Cannot resolve symbol '" + token.getText() + "'"); @@ -354,6 +371,27 @@ private MethodInfo findMethodDeclarationContainingPosition(Token token) { } return null; } + + private ScriptTypeInfo findScriptTypeContainingPosition(Token token) { + ScriptLine line = token.getParentLine(); + if (line == null || line.getParent() == null) { + return null; + } + + ScriptDocument doc = line.getParent(); + int tokenStart = token.getGlobalStart(); + + for (ScriptTypeInfo scriptType : doc.getScriptTypes()) { + int typeStart = scriptType.getDeclarationOffset(); + int typeEnd = scriptType.getBodyStart(); + + // Token is within the type declaration + if (tokenStart >= typeStart && tokenStart <= typeEnd) { + return scriptType; + } + } + return null; + } /** * Find the argument that contains the given position. @@ -539,7 +577,7 @@ private void extractClassInfo(Token token) { } // Add ScriptTypeInfo errors - if (scriptType.hasError()) { + if (false) { //|| scriptType.hasError() // Missing interface method errors for (ScriptTypeInfo.MissingMethodError err : scriptType.getMissingMethodErrors()) { errors.add(err.getMessage()); From 861cdcd533f33699ab6b35b1d70f3d4514d7ae65 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 21:15:18 +0200 Subject: [PATCH 127/337] Moved all inheritance code to ScriptTypeInfo --- .../script/interpreter/ScriptDocument.java | 122 +-------- .../interpreter/method/MethodSignature.java | 14 + .../interpreter/type/ScriptTypeInfo.java | 241 +++++++++++++++--- .../script/interpreter/type/TypeInfo.java | 8 + 4 files changed, 222 insertions(+), 163 deletions(-) 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 2842a57c0..118521741 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 @@ -4217,7 +4217,7 @@ private void detectMethodInheritance() { for (ScriptTypeInfo scriptType : scriptTypes.values()) { scriptType.clearErrors(); // Clear previous errors detectMethodInheritanceForType(scriptType); - validateScriptType(scriptType); // Validate after detecting inheritance + scriptType.validate(); // Validate after detecting inheritance } } @@ -4255,126 +4255,6 @@ private void detectMethodInheritanceForType(ScriptTypeInfo scriptType) { } } - /** - * Validate a script type for missing interface methods and constructor matching. - */ - private void validateScriptType(ScriptTypeInfo scriptType) { - // Skip validation for interfaces - they don't implement methods - if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) - return; - - // Check that all interface methods are implemented - if (scriptType.hasImplementedInterfaces()) { - for (TypeInfo iface : scriptType.getImplementedInterfaces()) { - if (iface == null || !iface.isResolved()) { - // Mark unresolved interface error - scriptType.setError(ScriptTypeInfo.ErrorType.UNRESOLVED_INTERFACE, - "Cannot resolve interface"); - continue; - } - validateInterfaceImplementation(scriptType, iface); - } - } - - // Check that extending class has a matching constructor - if (scriptType.hasSuperClass()) { - TypeInfo superClass = scriptType.getSuperClass(); - if (superClass == null || !superClass.isResolved()) { - scriptType.setError(ScriptTypeInfo.ErrorType.UNRESOLVED_PARENT, - "Cannot resolve parent class " + scriptType.getSuperClassName()); - } else { - validateConstructorChain(scriptType, superClass); - } - } - } - - /** - * Validate that a script type implements all methods from an interface. - */ - private void validateInterfaceImplementation(ScriptTypeInfo scriptType, TypeInfo iface) { - // Get all methods from the interface - Class javaClass = iface.getJavaClass(); - if (javaClass == null || !javaClass.isInterface()) - return; - - try { - for (java.lang.reflect.Method javaMethod : javaClass.getMethods()) { - // Skip static and default methods - if (java.lang.reflect.Modifier.isStatic(javaMethod.getModifiers())) - continue; - - // Check if scriptType has a matching method - String methodName = javaMethod.getName(); - int paramCount = javaMethod.getParameterCount(); - - boolean found = false; - List overloads = scriptType.getAllMethodOverloads(methodName); - for (MethodInfo method : overloads) { - if (parameterTypesMatch(method, javaMethod)) { - found = true; - break; - } - } - - if (!found) { - // Build signature string for error message - StringBuilder sig = new StringBuilder("("); - Class[] paramTypes = javaMethod.getParameterTypes(); - for (int i = 0; i < paramTypes.length; i++) { - if (i > 0) - sig.append(", "); - sig.append(paramTypes[i].getSimpleName()); - } - sig.append(")"); - - scriptType.addMissingMethodError(iface, methodName, sig.toString()); - } - } - } catch (Exception e) { - // Security or linkage error - } - } - - /** - * Validate that a script type has a constructor compatible with its parent class. - * This checks that for each parent constructor, there's a matching constructor in the child. - */ - private void validateConstructorChain(ScriptTypeInfo scriptType, TypeInfo superClass) { - // If no constructors defined in script type, it has an implicit default constructor - // Check if parent has a no-arg constructor - if (!scriptType.hasConstructors()) { - boolean parentHasNoArg = false; - - if (superClass instanceof ScriptTypeInfo) { - ScriptTypeInfo parentScript = (ScriptTypeInfo) superClass; - if (!parentScript.hasConstructors() || parentScript.findConstructor(0) != null) { - parentHasNoArg = true; - } - } else { - Class javaClass = superClass.getJavaClass(); - if (javaClass != null) { - try { - for (java.lang.reflect.Constructor ctor : javaClass.getConstructors()) { - if (ctor.getParameterCount() == 0) { - parentHasNoArg = true; - break; - } - } - } catch (Exception e) { - // Security error - } - } - } - - if (!parentHasNoArg) { - scriptType.addConstructorMismatchError(superClass, - superClass.getSimpleName() + "()"); - } - } - // If script type has constructors, we'd need to check super() calls - that's more complex - // For now, we just validate the implicit default constructor case - } - /** * Search for a matching method in a type hierarchy (class inheritance). * @param type The type to search in (and its superclasses) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodSignature.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodSignature.java index ba7d3e61a..5cde0ac63 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodSignature.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodSignature.java @@ -2,6 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; @@ -78,4 +79,17 @@ public String toString() { sb.append(")"); return sb.toString(); } + + public static String asString(Method javaMethod) { + StringBuilder sig = new StringBuilder(javaMethod.getName()); + sig.append("("); + Class[] paramTypes = javaMethod.getParameterTypes(); + for (int i = 0; i < paramTypes.length; i++) { + if (i > 0) + sig.append(", "); + sig.append(paramTypes[i].getSimpleName()); + } + sig.append(")"); + return sig.toString(); + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index 0f84772f5..7febf5dc1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -2,8 +2,11 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -347,45 +350,6 @@ public enum ErrorType { /** * Represents a missing interface method error. */ - public static class MissingMethodError { - private final TypeInfo interfaceType; - private final String methodName; - private final String signature; - - public MissingMethodError(TypeInfo interfaceType, String methodName, String signature) { - this.interfaceType = interfaceType; - this.methodName = methodName; - this.signature = signature; - } - - public TypeInfo getInterfaceType() { return interfaceType; } - public String getMethodName() { return methodName; } - public String getSignature() { return signature; } - - public String getMessage() { - return "Class must implement method '" + methodName + signature + "' from interface " + interfaceType.getSimpleName(); - } - } - - /** - * Represents a constructor mismatch error. - */ - public static class ConstructorMismatchError { - private final TypeInfo parentType; - private final String parentConstructorSignature; - - public ConstructorMismatchError(TypeInfo parentType, String parentConstructorSignature) { - this.parentType = parentType; - this.parentConstructorSignature = parentConstructorSignature; - } - - public TypeInfo getParentType() { return parentType; } - public String getParentConstructorSignature() { return parentConstructorSignature; } - - public String getMessage() { - return "Class extends " + parentType.getSimpleName() + " but has no constructor matching " + parentConstructorSignature; - } - } // Error tracking private ErrorType errorType = ErrorType.NONE; @@ -411,8 +375,8 @@ public void setError(ErrorType type, String message) { /** * Add a missing interface method error. */ - public void addMissingMethodError(TypeInfo interfaceType, String methodName, String signature) { - missingMethodErrors.add(new MissingMethodError(interfaceType, methodName, signature)); + public void addMissingMethodError(TypeInfo interfaceType, String signature) { + missingMethodErrors.add(new MissingMethodError(interfaceType, signature)); } /** @@ -431,4 +395,197 @@ public void clearErrors() { missingMethodErrors.clear(); constructorMismatchErrors.clear(); } -} + + public static class MissingMethodError { + private final TypeInfo interfaceType; + private final String signature; + + public MissingMethodError(TypeInfo interfaceType, String signature) { + this.interfaceType = interfaceType; + this.signature = signature; + } + + public String getMessage() { + return "Class must implement method '" + signature + "' from interface " + interfaceType.getSimpleName(); + } + } + + /** + * Represents a constructor mismatch error. + */ + public static class ConstructorMismatchError { + private final TypeInfo parentType; + private final String parentConstructorSignature; + + public ConstructorMismatchError(TypeInfo parentType, String parentConstructorSignature) { + this.parentType = parentType; + this.parentConstructorSignature = parentConstructorSignature; + } + + public String getMessage() { + return "Class extends " + parentType.getSimpleName() + " but has no constructor matching " + parentConstructorSignature; + } + } + + // ==================== VALIDATION ==================== + + /** + * Validate this script type for missing interface methods and constructor matching. + */ + @Override + public void validate() { + // Check that extending class has a matching constructor + if (hasSuperClass()) { + TypeInfo superClass = getSuperClass(); + if (superClass == null || !superClass.isResolved()) { + setError(ErrorType.UNRESOLVED_PARENT, + "Cannot resolve parent class " + getSuperClassName()); + } else { + validateConstructorChain(superClass); + } + } + + // Skip validation for interfaces - they don't implement methods + if (getKind() == Kind.INTERFACE) + return; + + // Check that all interface methods are implemented + if (hasImplementedInterfaces()) { + for (TypeInfo iface : getImplementedInterfaces()) { + if (iface == null || !iface.isResolved()) { + // Mark unresolved interface error + setError(ErrorType.UNRESOLVED_INTERFACE, "Cannot resolve interface"); + continue; + } + validateInterfaceImplementation(iface); + } + } + } + + /** + * Validate that this script type implements all methods from an interface. + */ + private void validateInterfaceImplementation(TypeInfo iface) { + // Handle Java interfaces + Class javaClass = iface.getJavaClass(); + if (javaClass != null && javaClass.isInterface()) { + try { + for (Method javaMethod : javaClass.getMethods()) { + // Skip static and default methods + if (Modifier.isStatic(javaMethod.getModifiers())) + continue; + + // Check if this type has a matching method + String methodName = javaMethod.getName(); + int paramCount = javaMethod.getParameterCount(); + + boolean found = false; + List overloads = getAllMethodOverloads(methodName); + for (MethodInfo method : overloads) { + if (parameterTypesMatch(method, javaMethod)) { + found = true; + break; + } + } + + //Limit to one error at a time to not spam all missing methods + if (!found && missingMethodErrors.isEmpty()) + addMissingMethodError(iface, MethodSignature.asString(javaMethod)); + } + } catch (Exception e) { + } + return; + } + + // Handle script-defined interfaces (ScriptType) + if (iface instanceof ScriptTypeInfo) { + ScriptTypeInfo ifaceType = (ScriptTypeInfo) iface; + + // Check all methods declared in the interface + for (MethodInfo ifaceMethod : ifaceType.getAllMethodsFlat()) { + MethodSignature ifaceSignature = ifaceMethod.getSignature(); + String methodName = ifaceMethod.getName(); + + boolean found = false; + List overloads = getAllMethodOverloads(methodName); + for (MethodInfo method : overloads) { + if (method.getSignature().equals(ifaceSignature)) { + found = true; + break; + } + } + + //Limit to one error at a time to not spam all missing methods + if (!found && missingMethodErrors.isEmpty()) + addMissingMethodError(iface, ifaceSignature.toString()); + } + } + } + + /** + * Validate that this script type has a constructor compatible with its parent class. + * This checks that for each parent constructor, there's a matching constructor in the child. + */ + private void validateConstructorChain(TypeInfo superClass) { + // If no constructors defined in script type, it has an implicit default constructor + // Check if parent has a no-arg constructor + if (!hasConstructors()) { + boolean parentHasNoArg = false; + + if (superClass instanceof ScriptTypeInfo) { + ScriptTypeInfo parentScript = (ScriptTypeInfo) superClass; + if (!parentScript.hasConstructors() || parentScript.findConstructor(0) != null) { + parentHasNoArg = true; + } + } else { + Class javaClass = superClass.getJavaClass(); + if (javaClass != null) { + try { + for (java.lang.reflect.Constructor ctor : javaClass.getConstructors()) { + if (ctor.getParameterCount() == 0) { + parentHasNoArg = true; + break; + } + } + } catch (Exception e) { + // Security error + } + } + } + + if (!parentHasNoArg) { + addConstructorMismatchError(superClass, superClass.getSimpleName()); + } + } + // If script type has constructors, we'd need to check super() calls - that's more complex + // For now, we just validate the implicit default constructor case + } + + /** + * Check if a MethodInfo's parameter types match a Java reflection Method's parameter types. + */ + private boolean parameterTypesMatch(MethodInfo methodInfo, Method javaMethod) { + List params = methodInfo.getParameters(); + Class[] javaParams = javaMethod.getParameterTypes(); + + if (params.size() != javaParams.length) + return false; + + for (int i = 0; i < params.size(); i++) { + TypeInfo paramType = params.get(i).getDeclaredType(); + if (paramType == null) + continue; // Unresolved param, skip check + + Class javaParamClass = javaParams[i]; + String javaParamName = javaParamClass.getName(); + + // Compare type names + if (!paramType.getFullName().equals(javaParamName) && + !paramType.getSimpleName().equals(javaParamClass.getSimpleName())) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 7e2b512c2..9ec721ab3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -461,6 +461,14 @@ public FieldInfo getFieldInfo(String fieldName) { return null; } + /** + * Validate this type. Default implementation does nothing (for Java types). + * Override in ScriptTypeInfo to validate script-defined types. + */ + public void validate() { + // Default: no validation for Java types + } + @Override public String toString() { return "TypeInfo{" + fullName + ", " + kind + ", resolved=" + resolved + "}"; From 7b77c1935fb2970bf9adc7b5b741c1dd7bb69f70 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 23:36:44 +0200 Subject: [PATCH 128/337] Added super() error validation --- .../interpreter/ErrorUnderlineRenderer.java | 15 +- .../script/interpreter/ScriptDocument.java | 196 +++++++++++- .../interpreter/type/ScriptTypeInfo.java | 291 ++++++++++++++++++ .../script/interpreter/type/TypeInfo.java | 32 +- 4 files changed, 515 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java index b9778e6ab..b5c8cfd38 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java @@ -72,18 +72,21 @@ public static void drawErrorUnderlines( int methodEnd = methodStart + call.getMethodName().length(); drawUnderlineForSpan(methodStart, methodEnd, lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); - } - - // Handle return type mismatch (underline the method name) - currently commented out - // if (call.hasReturnTypeMismatch()) { ... } + } else if (call.hasArgTypeError()) { + // Handle return type mismatch (underline the method name) - currently commented out + // if (call.hasReturnTypeMismatch()) { ... } - // Handle arg type errors (underline specific arguments) - if (call.hasArgTypeError()) { + // Handle arg type errors (underline specific arguments) + for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) { MethodCallInfo.Argument arg = error.getArg(); drawUnderlineForSpan(arg.getStartOffset(), arg.getEndOffset(), lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); } + } else if(call.hasError()){ + // Handle other call errors (underline the whole call) + drawUnderlineForSpan(call.getMethodNameStart(), call.getCloseParenOffset() + 1, + lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR); } } 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 118521741..740870e71 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 @@ -56,7 +56,7 @@ public class ScriptDocument { private static final Pattern MODIFIER_PATTERN = Pattern.compile( "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); private static final Pattern KEYWORD_PATTERN = Pattern.compile( - "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|super|new|typeof|instanceof|import)\\b"); // Declarations - Updated to capture method parameters private static final Pattern IMPORT_PATTERN = Pattern.compile( @@ -1996,6 +1996,12 @@ private void markMethodCalls(List marks) { continue; } + // Handle super() constructor calls + if (methodName.equals("super")) { + handleSuperConstructorCall(marks, nameStart, nameEnd, openParen, closeParen); + continue; + } + // Parse the arguments (first pass - without expected types for overload resolution) List arguments = parseMethodArguments(openParen + 1, closeParen, null); @@ -2007,7 +2013,15 @@ private void markMethodCalls(List marks) { MethodInfo resolvedMethod = null; if (receiverType != null) { - if (receiverType.hasMethod(methodName)) { + // Check for method existence using hierarchy search if it's a ScriptTypeInfo + boolean hasMethod = false; + if (receiverType instanceof ScriptTypeInfo) { + hasMethod = ((ScriptTypeInfo) receiverType).hasMethodInHierarchy(methodName); + } else { + hasMethod = receiverType.hasMethod(methodName); + } + + if (hasMethod) { TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); // Get best method overload based on argument types @@ -2504,6 +2518,14 @@ private TypeInfo resolveExpressionType(String expr, int position) { return currentType; } } + } else if (first.name.equals("super")) { + // Resolve super to parent class + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasSuperClass()) { + currentType = enclosingType.getSuperClass(); + } else { + return null; // No parent class + } } else { // Check if first segment is a type name for static access TypeInfo typeCheck = resolveType(first.name); @@ -2782,19 +2804,42 @@ private TypeInfo resolveChainSegment(TypeInfo currentType, ChainSegment segment) if (segment.isMethodCall) { // Method call - get return type with argument-based overload resolution - if (currentType.hasMethod(segment.name)) { + // Check for method existence using hierarchy search if it's a ScriptTypeInfo + boolean hasMethod = false; + if (currentType instanceof ScriptTypeInfo) { + hasMethod = ((ScriptTypeInfo) currentType).hasMethodInHierarchy(segment.name); + } else { + hasMethod = currentType.hasMethod(segment.name); + } + + if (hasMethod) { // Parse argument types from the method call TypeInfo[] argTypes = parseArgumentTypes(segment.arguments, segment.start); // Get the best matching overload based on argument types + // getBestMethodOverload is now overridden in ScriptTypeInfo to search hierarchy MethodInfo methodInfo = currentType.getBestMethodOverload(segment.name, argTypes); return (methodInfo != null) ? methodInfo.getReturnType() : null; } return null; } else { - // Field access - if (currentType.hasField(segment.name)) { - FieldInfo fieldInfo = currentType.getFieldInfo(segment.name); + // Field access - use hierarchy search for ScriptTypeInfo + boolean hasField = false; + FieldInfo fieldInfo = null; + + if (currentType instanceof ScriptTypeInfo) { + hasField = ((ScriptTypeInfo) currentType).hasFieldInHierarchy(segment.name); + if (hasField) { + fieldInfo = ((ScriptTypeInfo) currentType).getFieldInfoInHierarchy(segment.name); + } + } else { + hasField = currentType.hasField(segment.name); + if (hasField) { + fieldInfo = currentType.getFieldInfo(segment.name); + } + } + + if (hasField) { return (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; } return null; @@ -3322,6 +3367,76 @@ private ScriptTypeInfo findEnclosingScriptType(int position) { return null; } + /** + * Find the constructor that contains the given position. + * Used for validating super() calls. + */ + private MethodInfo findEnclosingConstructor(int position) { + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType == null) + return null; + + for (MethodInfo constructor : enclosingType.getConstructors()) { + int bodyStart = constructor.getBodyStart(); + int bodyEnd = constructor.getBodyEnd(); + if (bodyStart >= 0 && bodyEnd > bodyStart && position >= bodyStart && position < bodyEnd) { + return constructor; + } + } + return null; + } + + /** + * Handle super() constructor calls with validation and argument matching. + */ + private void handleSuperConstructorCall(List marks, int nameStart, int nameEnd, int openParen, + int closeParen) { + // Validate that we're in a constructor + TokenErrorMessage errorMsg = null; + + //Parent class + ScriptTypeInfo enclosingType = findEnclosingScriptType(nameStart); + MethodInfo enclosingConstructor = findEnclosingConstructor(nameStart); + TypeInfo superClass = enclosingType != null ? enclosingType.getSuperClass() : null; + + // Parse arguments and find matching parent constructor + List arguments = parseMethodArguments(openParen + 1, closeParen, null); + TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); + + // Find matching constructor in parent class + MethodInfo parentConstructor = superClass != null ? superClass.findConstructor(argTypes) : null; + + if (enclosingType == null || !enclosingType.hasSuperClass()) { + // No parent class - error + errorMsg = TokenErrorMessage + .from(enclosingType == null ? "'super()' can only be used within a class" : "Class '" + enclosingType.getSimpleName() + "' does not have a parent class") + .clearOtherErrors(); + } else if (enclosingConstructor == null) { + // Not in a constructor - error + errorMsg = TokenErrorMessage.from("Call to 'super()' only allowed in constructor body").clearOtherErrors(); + } else if (superClass == null || !superClass.isResolved()) { + // Parent class not resolved - error + errorMsg = TokenErrorMessage.from( + "Cannot resolve parent class '" + enclosingType.getSuperClassName() + "'"); + } else if (parentConstructor == null) { + // No matching constructor found + String argTypeStr = java.util.Arrays.stream(argTypes) + .map(t -> t != null ? t.getSimpleName() : "unknown") + .collect(java.util.stream.Collectors.joining(", ")); + errorMsg = TokenErrorMessage + .from("No constructor found in '" + superClass.getSimpleName() + "' matching super(" + argTypeStr + ")") + .clearOtherErrors(); + } + + // Successfully resolved - mark as a valid method call (constructor call) + MethodCallInfo callInfo = new MethodCallInfo("super", nameStart, nameEnd, openParen, closeParen, arguments, + superClass, parentConstructor, false).setConstructor(true); + callInfo.validate(); + methodCalls.add(callInfo); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.IMPORT_KEYWORD, + errorMsg != null ? errorMsg : callInfo)); + } + /** * Check if a method name is defined in this script. */ @@ -3540,9 +3655,10 @@ private void markVariables(List marks) { * Does NOT mark method calls (identifiers followed by parentheses) - those are handled by markMethodCalls. */ private void markChainedFieldAccesses(List marks) { - // Pattern to find identifier chains: identifier.identifier, this.identifier, etc. - // Start with an identifier or 'this' followed by at least one dot and another identifier - Pattern chainPattern = Pattern.compile("\\b(this|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + // Pattern to find identifier chains: identifier.identifier, this.identifier, super.identifier, etc. + // Start with an identifier, 'this', or 'super' followed by at least one dot and another identifier + Pattern chainPattern = Pattern.compile( + "\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); Matcher m = chainPattern.matcher(text); while (m.find()) { @@ -3618,6 +3734,7 @@ private void markChainedFieldAccesses(List marks) { // Start with resolving the first segment TypeInfo currentType = null; boolean firstIsThis = firstSegment.equals("this"); + boolean firstIsSuper = firstSegment.equals("super"); boolean firstIsPrecededByDot = isPrecededByDot(chainStart); // Determine the starting index for marking @@ -3628,6 +3745,28 @@ private void markChainedFieldAccesses(List marks) { // For 'this', we don't have a class context to resolve, but we can mark subsequent fields // as global fields if they exist in globalFields currentType = null; // We'll use globalFields for the next segment + } else if (firstIsSuper) { + // For 'super', resolve to parent class type + ScriptTypeInfo enclosingType = findEnclosingScriptType(chainStart); + if (enclosingType != null && enclosingType.hasSuperClass()) { + currentType = enclosingType.getSuperClass(); + if (currentType == null || !currentType.isResolved()) { + // Mark 'super' as error - parent class not resolved + marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1], + TokenType.UNDEFINED_VAR, TokenErrorMessage + .from("Cannot resolve parent class '" + enclosingType.getSuperClassName() + "'") + .clearOtherErrors())); + return; // Can't continue + } + } else { + // Mark 'super' as error - no parent class + String errorMsg = enclosingType == null ? "'super' can only be used within a class" : "Class '" + enclosingType.getSimpleName() + "' does not have a parent class"; + marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1], + TokenType.UNDEFINED_VAR, TokenErrorMessage.from(errorMsg).clearOtherErrors())); + return; // Can't continue + } + marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1], + TokenType.IMPORT_KEYWORD, currentType)); } else { // Try to resolve as a type first (class/interface/enum name) TypeInfo typeCheck = resolveType(firstSegment); @@ -3698,6 +3837,38 @@ private void markChainedFieldAccesses(List marks) { marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); currentType = null; } + } else if (i == 1 && firstIsSuper) { + // For "super.field", currentType is already the parent class + // Search recursively through the inheritance hierarchy + boolean found = false; + FieldInfo fieldInfo = null; + + if (currentType != null) { + if (currentType instanceof ScriptTypeInfo) { + found = ((ScriptTypeInfo) currentType).hasFieldInHierarchy(segment); + if (found) { + fieldInfo = ((ScriptTypeInfo) currentType).getFieldInfoInHierarchy(segment); + } + } else { + // For Java classes, hasField already checks inheritance + found = currentType.hasField(segment); + if (found) { + fieldInfo = currentType.getFieldInfo(segment); + } + } + } + + if (found) { + FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], currentType, + fieldInfo, isLastSegment, false); + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); + currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; + } else { + String errorMsg = currentType != null ? "Field '" + segment + "' not found in parent class hierarchy starting from '" + currentType.getSimpleName() + "'" : "Cannot resolve field '" + segment + "'"; + marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, + TokenErrorMessage.from(errorMsg).clearOtherErrors())); + currentType = null; + } } else if (currentType != null && currentType.isResolved()) { // Check if this type has this field if (currentType.hasField(segment)) { @@ -4556,9 +4727,12 @@ private void markImportedClassUsages(List marks) { List arguments = parseMethodArguments(openParen + 1, closeParen, null); int argCount = arguments.size(); - + TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType) + .toArray(TypeInfo[]::new); + + // Try to find matching constructor (may be null if not found) - MethodInfo constructor = info.hasConstructors() ? info.findConstructor(argCount) : null; + MethodInfo constructor = info.hasConstructors() ? info.findConstructor(argTypes) : null; // Create MethodCallInfo for constructor (even if null, so errors are tracked) MethodCallInfo ctorCall = MethodCallInfo.constructor( diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index 7febf5dc1..a9bcc0333 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -288,6 +288,297 @@ public MethodInfo findConstructor(int argCount) { } return null; } + + public MethodInfo findConstructor(TypeInfo[] argTypes) { + for (MethodInfo constructor : constructors) { + if (constructor.getParameterCount() == argTypes.length) { + boolean match = true; + List params = constructor.getParameters(); + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + if (!TypeChecker.isTypeCompatible(paramType, argTypes[i])) { + match = false; + break; + } + } + if (match) + return constructor; + } + } + return null; + } + + // ==================== INHERITANCE HIERARCHY SEARCH ==================== + + /** + * Check if this type or any of its parent classes has a field with the given name. + * This recursively searches up the inheritance tree. + */ + public boolean hasFieldInHierarchy(String fieldName) { + // Check this class first + if (hasField(fieldName)) { + return true; + } + + // Check parent class recursively + if (superClass != null && superClass.isResolved()) { + if (superClass instanceof ScriptTypeInfo) { + return ((ScriptTypeInfo) superClass).hasFieldInHierarchy(fieldName); + } else { + // For Java classes, hasField already checks inheritance via reflection + return superClass.hasField(fieldName); + } + } + + return false; + } + + /** + * Get field info from this type or any of its parent classes. + * This recursively searches up the inheritance tree. + */ + public FieldInfo getFieldInfoInHierarchy(String fieldName) { + // Check this class first + FieldInfo field = getFieldInfo(fieldName); + if (field != null) { + return field; + } + + // Check parent class recursively + if (superClass != null && superClass.isResolved()) { + if (superClass instanceof ScriptTypeInfo) { + return ((ScriptTypeInfo) superClass).getFieldInfoInHierarchy(fieldName); + } else { + // For Java classes, getFieldInfo already checks inheritance via reflection + return superClass.getFieldInfo(fieldName); + } + } + + return null; + } + + /** + * Check if this type or any of its parent classes has a method with the given name. + * This recursively searches up the inheritance tree. + */ + public boolean hasMethodInHierarchy(String methodName) { + // Check this class first + if (hasMethod(methodName)) { + return true; + } + + // Check parent class recursively + if (superClass != null && superClass.isResolved()) { + if (superClass instanceof ScriptTypeInfo) { + return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName); + } else { + // For Java classes, hasMethod already checks inheritance via reflection + return superClass.hasMethod(methodName); + } + } + + return false; + } + + /** + * Check if this type or any of its parent classes has a method with the given name and parameter count. + * This recursively searches up the inheritance tree. + */ + public boolean hasMethodInHierarchy(String methodName, int paramCount) { + // Check this class first + if (hasMethod(methodName, paramCount)) { + return true; + } + + // Check parent class recursively + if (superClass != null && superClass.isResolved()) { + if (superClass instanceof ScriptTypeInfo) { + return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName, paramCount); + } else { + // For Java classes, hasMethod already checks inheritance via reflection + return superClass.hasMethod(methodName, paramCount); + } + } + + return false; + } + + /** + * Get method info from this type or any of its parent classes. + * This recursively searches up the inheritance tree. + */ + public MethodInfo getMethodInfoInHierarchy(String methodName) { + // Check this class first + MethodInfo method = getMethodInfo(methodName); + if (method != null) { + return method; + } + + // Check parent class recursively + if (superClass != null && superClass.isResolved()) { + if (superClass instanceof ScriptTypeInfo) { + return ((ScriptTypeInfo) superClass).getMethodInfoInHierarchy(methodName); + } else { + // For Java classes, getMethodInfo already checks inheritance via reflection + return superClass.getMethodInfo(methodName); + } + } + + return null; + } + + /** + * Get method info with specific parameter count from this type or any of its parent classes. + * This recursively searches up the inheritance tree. + */ + public MethodInfo getMethodWithParamCountInHierarchy(String methodName, int paramCount) { + // Check this class first + MethodInfo method = getMethodWithParamCount(methodName, paramCount); + if (method != null) { + return method; + } + + // Check parent class recursively + if (superClass != null && superClass.isResolved()) { + if (superClass instanceof ScriptTypeInfo) { + return ((ScriptTypeInfo) superClass).getMethodWithParamCountInHierarchy(methodName, paramCount); + } else { + // For Java classes, reflection already handles inheritance + // We need to manually search for matching method + List overloads = superClass.getAllMethodOverloads(methodName); + for (MethodInfo m : overloads) { + if (m.getParameterCount() == paramCount) { + return m; + } + } + } + } + + return null; + } + + /** + * Get all method overloads with the given name from this type or any of its parent classes. + * This recursively searches up the inheritance tree and returns all matching overloads. + */ + public List getAllMethodOverloadsInHierarchy(String methodName) { + List result = new ArrayList<>(); + + // Get overloads from this class + result.addAll(getAllMethodOverloads(methodName)); + + // Get overloads from parent class recursively + if (superClass != null && superClass.isResolved()) { + if (superClass instanceof ScriptTypeInfo) { + result.addAll(((ScriptTypeInfo) superClass).getAllMethodOverloadsInHierarchy(methodName)); + } else { + // For Java classes, getAllMethodOverloads already checks inheritance via reflection + result.addAll(superClass.getAllMethodOverloads(methodName)); + } + } + + return result; + } + + /** + * Override getBestMethodOverload to search the inheritance hierarchy. + * This ensures that method overload resolution considers parent class methods. + */ + @Override + public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) { + // Get all overloads from this class and parent classes + List allOverloads = getAllMethodOverloadsInHierarchy(methodName); + if (allOverloads.isEmpty()) + return null; + + // If no arguments provided, try to find zero-arg method + if (argTypes == null || argTypes.length == 0) { + for (MethodInfo method : allOverloads) { + if (method.getParameterCount() == 0) { + return method; + } + } + // Fall back to first overload if no zero-arg found + return allOverloads.get(0); + } + + // Phase 1: Try exact match + for (MethodInfo method : allOverloads) { + if (method.getParameterCount() == argTypes.length) { + boolean exactMatch = true; + List params = method.getParameters(); + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = argTypes[i]; + if (paramType == null || argType == null || !paramType.equals(argType)) { + exactMatch = false; + break; + } + } + if (exactMatch) + return method; + } + } + + // Phase 2: Try compatible type match (assignability) + for (MethodInfo method : allOverloads) { + if (method.getParameterCount() == argTypes.length) { + boolean compatible = true; + List params = method.getParameters(); + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = argTypes[i]; + if (paramType == null || argType == null) { + continue; // Allow null types (unresolved) + } + // Check if argType can be assigned to paramType + if (!noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible(paramType, + argType)) { + compatible = false; + break; + } + } + if (compatible) + return method; + } + } + + // Phase 3: Fall back to first overload with matching parameter count + for (MethodInfo method : allOverloads) { + if (method.getParameterCount() == argTypes.length) { + return method; + } + } + + // No match found, return first overload + return allOverloads.get(0); + } + + /** + * Override getBestMethodOverload with return type expectation to search the inheritance hierarchy. + */ + @Override + public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedReturnType) { + List allOverloads = getAllMethodOverloadsInHierarchy(methodName); + if (allOverloads.isEmpty()) + return null; + + // If no expected return type, return first overload + if (expectedReturnType == null) { + return allOverloads.get(0); + } + + // First pass: look for return type compatible overload + for (MethodInfo method : allOverloads) { + TypeInfo returnType = method.getReturnType(); + if (returnType != null && noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible( + expectedReturnType, returnType)) { + return method; + } + } + // Second pass: return any overload (first one) + return allOverloads.get(0); + } // ==================== INNER CLASS LOOKUP ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 9ec721ab3..2d9d97252 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -4,6 +4,7 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; @@ -226,8 +227,8 @@ public MethodInfo findConstructor(int argCount) { if (javaClass == null) return null; try { - java.lang.reflect.Constructor[] constructors = javaClass.getConstructors(); - for (java.lang.reflect.Constructor ctor : constructors) { + Constructor[] constructors = javaClass.getConstructors(); + for (Constructor ctor : constructors) { if (ctor.getParameterCount() == argCount) { return MethodInfo.fromReflectionConstructor(ctor, this); } @@ -237,6 +238,33 @@ public MethodInfo findConstructor(int argCount) { } return null; } + + public MethodInfo findConstructor(TypeInfo[] argTypes) { + if (javaClass == null) return null; + + try { + Constructor[] constructors = javaClass.getConstructors(); + for (Constructor ctor : constructors) { + if (ctor.getParameterCount() == argTypes.length) { + Class[] paramTypes = ctor.getParameterTypes(); + boolean match = true; + for (int i = 0; i < argTypes.length; i++) { + TypeInfo paramTypeInfo = TypeInfo.fromClass(paramTypes[i]); + if (!TypeChecker.isTypeCompatible(paramTypeInfo, argTypes[i])) { + match = false; + break; + } + } + if (match) { + return MethodInfo.fromReflectionConstructor(ctor, this); + } + } + } + } catch (Exception e) { + // Security or linkage error + } + return null; + } /** * Check if this type has a field with the given name. From 275a5f10c9ef95b7de802263ba81a5cc5e7549ee Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 1 Jan 2026 01:01:47 +0200 Subject: [PATCH 129/337] Retrieved true containing type of method calls in hover info (handled inheritance case) --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 253e75086..8335ab6a6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -621,6 +621,13 @@ private void extractMethodCallInfo(Token token) { TypeInfo containingType = null; if (callInfo != null) { containingType = callInfo.getReceiverType(); + + //Checks true containing type from resolved method (handles inheritance cases) + if (callInfo.getResolvedMethod() != null) { + TypeInfo trueContainingType = callInfo.getResolvedMethod().getContainingType(); + if (trueContainingType != null) + containingType = trueContainingType; + } } if (containingType == null && methodInfo != null) { containingType = methodInfo.getContainingType(); From 86da3f21e14094d6099ea2e1d691206cba534c0b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 1 Jan 2026 14:39:41 +0200 Subject: [PATCH 130/337] Added bold/italic rendering for TokenType --- .../gui/util/script/interpreter/ScriptLine.java | 16 +++++++++++----- .../gui/util/script/interpreter/token/Token.java | 11 +++++++++++ .../util/script/interpreter/token/TokenType.java | 16 ++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index c262a6877..57a13ed78 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -226,6 +226,7 @@ public void addIndentGuide(int column) { * Draw this line with syntax highlighting using Minecraft color codes. * Compatible with the existing rendering system. * Also draws curly underlines for tokens with errors (method call validation failures). + * Supports bold (§l) and italic (§o) formatting for certain token types. */ public void drawString(int x, int y, int defaultColor) { StringBuilder builder = new StringBuilder(); @@ -246,12 +247,16 @@ public void drawString(int x, int y, int defaultColor) { } - // Append the colored token + // Append style codes (bold/italic) if applicable + String stylePrefix = t.getStylePrefix(); + + // Append the colored token with style builder.append(COLOR_CHAR) .append(t.getColorCode()) + .append(stylePrefix) .append(t.getText()) .append(COLOR_CHAR) - .append('f'); // Reset to white + .append('r'); // Reset all formatting (color + bold/italic) currentX += tokenWidth; lastIndex = tokenStart + t.getText().length(); @@ -295,10 +300,11 @@ public void drawStringHex(int x, int y) { } // Add the colored token - segments.add(new TextSegment(fullText.length(), t.getText(), t.getHexColor(), true)); - fullText.append(t.getText()); + String styledText = t.getStylePrefix() + t.getText(); + segments.add(new TextSegment(fullText.length(), styledText, t.getHexColor(), true)); + fullText.append(styledText); - lastIndex = tokenStart + t.getText().length(); + lastIndex = tokenStart + styledText.length(); } // Add any remaining text after the last token diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index 69b89c15b..0371ee17f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -248,6 +248,17 @@ public int getHexColor() { public char getColorCode() { return type.toColorCode(); } + + /** + * Get the Minecraft style prefix for this token (bold/italic codes). + * Returns empty string if no style, otherwise §l for bold, §o for italic, or both. + */ + public String getStylePrefix() { + StringBuilder sb = new StringBuilder(); + if (type.isBold()) sb.append('\u00A7').append('l'); + if (type.isItalic()) sb.append('\u00A7').append('o'); + return sb.toString(); + } @Override public String toString() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index b018c942f..0b7e8e252 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -45,10 +45,18 @@ public enum TokenType { private final int hexColor; private final int priority; + private final boolean bold; + private final boolean italic; TokenType(int hexColor, int priority) { + this(hexColor, priority, false, false); + } + + TokenType(int hexColor, int priority, boolean bold, boolean italic) { this.hexColor = hexColor; this.priority = priority; + this.bold = bold; + this.italic = italic; } public int getHexColor() { @@ -59,6 +67,14 @@ public int getPriority() { return priority; } + public boolean isBold() { + return bold; + } + + public boolean isItalic() { + return italic; + } + public static int getColor(TypeInfo typeInfo) { if (typeInfo == null) return TokenType.IMPORTED_CLASS.getHexColor(); From 58cac3667c02f225c5881c29cce041d5ebc3a60b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 14:57:45 +0200 Subject: [PATCH 131/337] Added ENUM CONSTANT parsing and validation --- .../interpreter/ErrorUnderlineRenderer.java | 3 +- .../script/interpreter/ScriptDocument.java | 79 +++++++++++++++++-- .../script/interpreter/field/FieldInfo.java | 66 +++++++++++++++- .../interpreter/hover/TokenHoverInfo.java | 29 ++++++- .../interpreter/method/MethodCallInfo.java | 37 +++++---- .../util/script/interpreter/token/Token.java | 3 +- .../script/interpreter/token/TokenType.java | 13 ++- .../interpreter/type/ScriptTypeInfo.java | 44 +++++++++++ 8 files changed, 244 insertions(+), 30 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java index b5c8cfd38..1e3bae054 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ErrorUnderlineRenderer.java @@ -55,7 +55,8 @@ public static void drawErrorUnderlines( boolean isDeclaration = false; int methodStart = call.getMethodNameStart(); for (MethodInfo mi : doc.getAllMethods()) { - if (mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) { + //!call.isConstructor for enum declarations + if (!call.isConstructor() && mi.getDeclarationOffset() <= methodStart && mi.getBodyStart() >= methodStart) { isDeclaration = true; break; } 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 740870e71..5c23db663 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 @@ -5,6 +5,7 @@ import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionNode; import noppes.npcs.client.gui.util.script.interpreter.expression.ExpressionTypeResolver; import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -42,6 +43,7 @@ */ public class ScriptDocument { + public static ScriptDocument INSTANCE = null; // For easy access in expressions // ==================== PATTERNS ==================== private static final Pattern WORD_PATTERN = Pattern.compile("[\\p{L}\\p{N}_-]+|\\n|$"); @@ -196,6 +198,7 @@ public void init(int width, int height) { linesCount = lines.size(); totalHeight = linesCount * lineHeight; visibleLines = Math.max(height / lineHeight - 1, 1); + INSTANCE = this; } // ==================== TOKENIZATION ==================== @@ -283,7 +286,7 @@ private void mergeOverlappingRanges() { excludedRanges.addAll(merged); } - private boolean isExcluded(int position) { + public boolean isExcluded(int position) { for (int[] range : excludedRanges) { if (position >= range[0] && position < range[1]) { return true; @@ -498,6 +501,25 @@ private void parseScriptTypeMembers(ScriptTypeInfo scriptType) { documentation); scriptType.addConstructor(constructorInfo); } + + // Parse enum constants (for enum types only) + if (scriptType.getKind() == TypeInfo.Kind.ENUM) { + List constants = EnumConstantInfo.parseEnumConstants( + scriptType, + bodyText, + bodyStart + 1, + KEYWORD_PATTERN + ); + + for (EnumConstantInfo constant : constants) { + scriptType.addEnumConstant(constant); + if (constant.getConstructorCall() != null) { + + } + // methodCalls.add(constant.getConstructorCall()); + + } + } } /** @@ -1424,6 +1446,9 @@ private List buildMarks() { // Class/interface/enum declarations markClassDeclarations(marks); + // Enum constants (must be after class declarations so enums are known) + markEnumConstants(marks); + // Keywords and modifiers addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); addPatternMarks(marks, MODIFIER_PATTERN, TokenType.MODIFIER); @@ -1703,6 +1728,29 @@ private void markClassDeclarations(List marks) { } } + /** + * Mark enum constants with ENUM_CONSTANT token type. + * Adds marks for all enum constants in script-defined enums. + */ + private void markEnumConstants(List marks) { + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + if (scriptType.getKind() != TypeInfo.Kind.ENUM) { + continue; + } + + // Mark each enum constant + for (EnumConstantInfo constant : scriptType.getEnumConstants().values()) { + FieldInfo fieldInfo = constant.getFieldInfo(); + int start = fieldInfo.getDeclarationOffset(); + int end = start + fieldInfo.getName().length(); + + // Always use ENUM_CONSTANT token type (blue + bold + italic) + // Errors are shown via underline, not by changing the token type + marks.add(new ScriptLine.Mark(start, end, TokenType.ENUM_CONSTANT, constant)); + } + } + } + /** * Mark the extends clause with proper coloring. * The parent class is colored based on its resolved type. @@ -2243,7 +2291,7 @@ private int findMatchingParen(int openPos) { * @param methodInfo Optional MethodInfo to provide expected parameter types for validation * @return List of parsed arguments with resolved types */ - private List parseMethodArguments(int start, int end, MethodInfo methodInfo) { + public List parseMethodArguments(int start, int end, MethodInfo methodInfo) { List args = new ArrayList<>(); if (start >= end) { @@ -5005,19 +5053,40 @@ public List getScriptTypes() { public List getAllMethods() { List allMethods = new ArrayList<>(methods); - for (ScriptTypeInfo scriptType : scriptTypes.values()) + for (ScriptTypeInfo scriptType : scriptTypes.values()) { allMethods.addAll(scriptType.getAllMethodsFlat()); + // Include constructors so their parameters are recognized + allMethods.addAll(scriptType.getConstructors()); + } return allMethods; } public List getMethodCalls() { - return Collections.unmodifiableList(methodCalls); + List allCalls = new ArrayList<>(methodCalls); + + for (EnumConstantInfo constant : getAllEnumConstants()) { + if (constant.getConstructorCall() != null) + allCalls.add(constant.getConstructorCall()); + } + + + return allCalls; } public List getFieldAccesses() { return Collections.unmodifiableList(fieldAccesses); } - + + public List getAllEnumConstants() { + List enums = new ArrayList<>(); + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + if (!scriptType.hasEnumConstants()) + continue; + + enums.addAll(scriptType.getEnumConstants().values()); + } + return enums; + } /** * Find an assignment at the given position, prioritizing LHS over RHS. * Searches across all script fields and external field assignments. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 6fa9bd613..660dad234 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -21,7 +21,8 @@ public final class FieldInfo { public enum Scope { GLOBAL, // Class-level field LOCAL, // Local variable inside a method - PARAMETER // Method parameter + PARAMETER, // Method parameter + ENUM_CONSTANT // Enum constant value } private final String name; @@ -49,11 +50,17 @@ public enum Scope { // Declaration assignment (for initial value validation) private AssignmentInfo declarationAssignment; + + // For enum constants: the arguments passed to the constructor (null if no args or not enum constant) + private final String enumConstantArgs; + + // For enum constants: the containing enum type + private final TypeInfo containingEnumType; private FieldInfo(String name, Scope scope, TypeInfo declaredType, int declarationOffset, boolean resolved, MethodInfo containingMethod, String documentation, int initStart, int initEnd, int modifiers, - Field reflectionField) { + Field reflectionField, String enumConstantArgs, TypeInfo containingEnumType) { this.name = name; this.scope = scope; this.declaredType = declaredType; @@ -65,6 +72,17 @@ private FieldInfo(String name, Scope scope, TypeInfo declaredType, this.initEnd = initEnd; this.modifiers = modifiers; this.reflectionField = reflectionField; + this.enumConstantArgs = enumConstantArgs; + this.containingEnumType = containingEnumType; + } + + // Private constructor for backward compatibility + private FieldInfo(String name, Scope scope, TypeInfo declaredType, + int declarationOffset, boolean resolved, MethodInfo containingMethod, + String documentation, int initStart, int initEnd, int modifiers, + Field reflectionField) { + this(name, scope, declaredType, declarationOffset, resolved, containingMethod, + documentation, initStart, initEnd, modifiers, reflectionField, null, null); } // Factory methods @@ -129,6 +147,44 @@ public static FieldInfo fromReflection(Field field, TypeInfo containingType) { TypeInfo type = TypeInfo.fromClass(field.getType()); return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1, field.getModifiers(), field); } + + /** + * Create a FieldInfo for an enum constant. + * @param name The constant name (e.g., "NORTH") + * @param type The enum type itself + * @param declOffset The declaration position + * @param args The constructor arguments (or null if no args) + * @param containingEnum The enum type this constant belongs to + */ + public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, String args, TypeInfo containingEnum) { + // Enum constants are implicitly public static final + int modifiers = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL; + return new FieldInfo(name, Scope.ENUM_CONSTANT, type, declOffset, true, null, null, -1, -1, + modifiers, null, args, containingEnum); + } + + // ==================== ERROR HANDLING ==================== + + /** + * Check if this is an enum constant. + */ + public boolean isEnumConstant() { + return scope == Scope.ENUM_CONSTANT; + } + + /** + * Get the enum constant args (for enum constants only). + */ + public String getEnumConstantArgs() { + return enumConstantArgs; + } + + /** + * Get the containing enum type (for enum constants only). + */ + public TypeInfo getContainingEnumType() { + return containingEnumType; + } // ==================== ASSIGNMENT MANAGEMENT ==================== @@ -333,11 +389,17 @@ public TokenType getTokenType() { } switch (scope) { case GLOBAL: + // Static final fields get special highlighting + if (isStatic() && isFinal()) { + return TokenType.STATIC_FINAL_FIELD; + } return TokenType.GLOBAL_FIELD; case LOCAL: return TokenType.LOCAL_FIELD; case PARAMETER: return TokenType.PARAMETER; + case ENUM_CONSTANT: + return TokenType.ENUM_CONSTANT; default: return TokenType.VARIABLE; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 8335ab6a6..246ab8a55 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -2,6 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.*; import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -287,7 +288,12 @@ else if (methodDecl.hasError()) { errors.add(scriptType.getErrorMessage()); } } - + + EnumConstantInfo enumConst = findEnumConstantContainingPosition(token); + if (enumConst != null && enumConst.hasError()) { + errors.add(enumConst.getErrorMessage()); + } + if(token.getType() == TokenType.UNDEFINED_VAR) errors.add("Cannot resolve symbol '" + token.getText() + "'"); @@ -393,6 +399,27 @@ private ScriptTypeInfo findScriptTypeContainingPosition(Token token) { return null; } + private EnumConstantInfo findEnumConstantContainingPosition(Token token) { + ScriptLine line = token.getParentLine(); + if (line == null || line.getParent() == null) { + return null; + } + + ScriptDocument doc = line.getParent(); + int tokenStart = token.getGlobalStart(); + + for (EnumConstantInfo enumConst : doc.getAllEnumConstants()) { + int constStart = enumConst.getDeclarationOffset(); + int constEnd = constStart + enumConst.getName().length(); + + // Token is within the enum constant declaration + if (tokenStart >= constStart && tokenStart <= constEnd) { + return enumConst; + } + } + return null; + } + /** * Find the argument that contains the given position. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index 8b7668fdf..247c8b584 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -73,7 +73,6 @@ public String toString() { (valid ? "" : " INVALID: " + errorMessage) + "}"; } } - /** * Validation error type. */ @@ -307,8 +306,8 @@ public Argument getArg() { * Sets error information if validation fails. */ public void validate() { - if (resolvedMethod == null) { - if (isConstructor) { + if (isConstructor) { + if (resolvedMethod == null) { // For constructors, check if the type has any constructors at all if (receiverType != null && receiverType.hasConstructors()) { setError(ErrorType.WRONG_ARG_COUNT, @@ -317,8 +316,8 @@ public void validate() { setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve constructor for '" + methodName + "'"); } - } else { - setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve method '" + methodName + "'"); + } else if (receiverType != null && arguments.size() == resolvedMethod.getParameterCount()) { + validateArgTypeError(); } return; } @@ -343,9 +342,24 @@ public void validate() { } // Check each argument type - for (int i = 0; i < actualCount; i++) { + validateArgTypeError(); + + // Check return type compatibility with expected type (e.g., assignment LHS) + // ALREADY CHECKED AT AssignmentInfo LEVEL + if (expectedType != null && resolvedMethod != null) { + TypeInfo returnType = resolvedMethod.getReturnType(); + if (returnType != null && !TypeChecker.isTypeCompatible(expectedType, returnType)) { + // setError(ErrorType.RETURN_TYPE_MISMATCH, + // "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName()); + } + } + } + + public void validateArgTypeError() { + // Check each argument type + for (int i = 0; i < arguments.size(); i++) { Argument arg = arguments.get(i); - FieldInfo para = params.get(i); + FieldInfo para = resolvedMethod.getParameters().get(i); TypeInfo argType = arg.getResolvedType(); TypeInfo paramType = para.getDeclaredType(); @@ -360,15 +374,6 @@ public void validate() { setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'"); } } - - // Check return type compatibility with expected type (e.g., assignment LHS) - if (expectedType != null && resolvedMethod != null) { - TypeInfo returnType = resolvedMethod.getReturnType(); - if (returnType != null && !TypeChecker.isTypeCompatible(expectedType, returnType)) { - // setError(ErrorType.RETURN_TYPE_MISMATCH, - // "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName()); - } - } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index 0371ee17f..35f956f54 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -230,7 +230,8 @@ public boolean isMethodCall() { public boolean isField() { return type == TokenType.GLOBAL_FIELD || type == TokenType.LOCAL_FIELD || - type == TokenType.PARAMETER; + type == TokenType.PARAMETER || type == TokenType.STATIC_FINAL_FIELD || + type == TokenType.ENUM_CONSTANT; } // ==================== RENDERING ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index 0b7e8e252..c67174491 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -22,23 +22,25 @@ public enum TokenType { // Type declarations and references INTERFACE_DECL(0x55FFFF, 85), // interface names (aqua) ENUM_DECL(0xFF55FF, 85), // enum names (magenta) + ENUM_CONSTANT(0x55FFFF, 84, true, false), // enum constant values (blue, bold+italic) - like IntelliJ CLASS_DECL(0x00AAAA, 85), // class names in declarations IMPORTED_CLASS(0x00AAAA, 75), // imported class usages TYPE_DECL(0x00AAAA, 70), // package paths, type references - + // Methods METHOD_DECL(0x00AA00, 60), // method declarations (green) METHOD_CALL(0x55FF55, 50), // method calls (bright green) - + // Variables and fields UNDEFINED_VAR(0xAA0000, 20), // unresolved variables (dark red) - high priority PARAMETER(0x5555FF, 36), // method parameters (blue) GLOBAL_FIELD(0x55FFFF, 35), // class-level fields (aqua) LOCAL_FIELD(0xFFFF55, 25), // local variables (yellow) - + STATIC_FINAL_FIELD(0xFF55FF, 36, false, true), // static final fields (magenta, italic) + // Literals LITERAL(0x777777, 40), // numeric and boolean literals - + // Default VARIABLE(0xFFFFFF, 30), // generic variables DEFAULT(0xFFFFFF, 0); // default text color (white) @@ -113,7 +115,10 @@ public char toColorCode() { case MODIFIER: return '6'; // gold case ENUM_DECL: + case STATIC_FINAL_FIELD: return 'd'; // magenta + case ENUM_CONSTANT: + return '9'; // blue (same as PARAMETER) case INTERFACE_DECL: case GLOBAL_FIELD: return 'b'; // aqua diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index a9bcc0333..dece8ba1e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter.type; +import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; @@ -33,6 +34,9 @@ public class ScriptTypeInfo extends TypeInfo { private final List constructors = new ArrayList<>(); // List of constructors private final List innerClasses = new ArrayList<>(); + // Enum constants (for enum types only) - name -> EnumConstantInfo + private final Map enumConstants = new HashMap<>(); + // Parent class reference (for inner class resolution) private ScriptTypeInfo outerClass; @@ -197,6 +201,46 @@ public Map getFields() { return new HashMap<>(fields); } + // ==================== ENUM CONSTANT MANAGEMENT ==================== + + /** + * Add an enum constant to this enum type. + * Only valid for enum types. + */ + public void addEnumConstant(EnumConstantInfo constant) { + if (isEnum()) { + enumConstants.put(constant.getFieldInfo().getName(), constant); + } + } + + /** + * Check if this enum has a constant with the given name. + */ + public boolean hasEnumConstant(String constantName) { + return enumConstants.containsKey(constantName); + } + + /** + * Get an enum constant by name. + */ + public EnumConstantInfo getEnumConstant(String constantName) { + return enumConstants.get(constantName); + } + + /** + * Get all enum constants. + */ + public Map getEnumConstants() { + return new HashMap<>(enumConstants); + } + + /** + * Check if this is an enum type and has any constants. + */ + public boolean hasEnumConstants() { + return isEnum() && !enumConstants.isEmpty(); + } + // ==================== METHOD MANAGEMENT ==================== public void addMethod(MethodInfo method) { From 96c243f6bf497e6d027b5d0c95f4bb1316da4e71 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 14:58:01 +0200 Subject: [PATCH 132/337] OOPS --- .../interpreter/field/EnumConstantInfo.java | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java new file mode 100644 index 000000000..68b38c37f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java @@ -0,0 +1,277 @@ +package noppes.npcs.client.gui.util.script.interpreter.field; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents an enum constant declaration with its constructor call. + * Combines a FieldInfo (the constant itself) with a MethodCallInfo (the constructor call validation). + * This allows us to reuse existing method call validation logic for enum constructor matching. + */ +public class EnumConstantInfo { + + private final FieldInfo fieldInfo; + private final MethodCallInfo constructorCall; // Can be null if no args provided + private final ScriptTypeInfo enumType; + + private EnumConstantInfo(FieldInfo fieldInfo, MethodCallInfo constructorCall, ScriptTypeInfo enumType) { + this.fieldInfo = fieldInfo; + this.constructorCall = constructorCall; + this.enumType = enumType; + } + + /** + * Parse enum constants from an enum body. + * Returns a list of EnumConstantInfo objects, each representing one constant. + * + * @param enumType The enum type being parsed + * @param bodyText The text content of the enum body + * @param bodyOffset The absolute offset where the enum body starts + * @param keywordPattern Pattern to detect Java keywords (to skip them) + * @return List of parsed enum constants + */ + public static List parseEnumConstants( + ScriptTypeInfo enumType, + String bodyText, + int bodyOffset, + Pattern keywordPattern) { + + List constants = new ArrayList<>(); + + // Find where enum constants end (first semicolon not in parens, or first method/field declaration) + int constantsEnd = findEnumConstantsEnd(bodyText); + if (constantsEnd <= 0) { + // No semicolon found - entire body might be constants + constantsEnd = bodyText.length(); + } + + // Pattern to match enum constants: NAME or NAME() or NAME(args) + Pattern constantPattern = Pattern.compile( + "([A-Za-z_][a-zA-Z0-9_]*)\\s*(\\(([^)]*)\\))?"); + + Matcher m = constantPattern.matcher(bodyText); + int lastEnd = 0; + + while (m.find()) { + // Skip if we're past the constants section + if (m.start() >= constantsEnd) { + break; + } + + int absPos = bodyOffset + m.start(); + if (ScriptDocument.INSTANCE.isExcluded(absPos)) { + lastEnd = m.end(); + continue; + } + + // Check if this is at a valid position (after comma or at start) + String beforeMatch = bodyText.substring(lastEnd, m.start()).trim(); + if (!beforeMatch.isEmpty() && !beforeMatch.equals(",")) { + // Not a valid enum constant position - might be inside parens + continue; + } + + String constantName = m.group(1); + String argsClause = m.group(2); // includes parens if present + String args = m.group(3); // just the args without parens + + // Skip keywords + if (keywordPattern.matcher(constantName).matches()) { + lastEnd = m.end(); + continue; + } + + // Create FieldInfo for the enum constant + FieldInfo fieldInfo = FieldInfo.enumConstant( + constantName, + enumType, + absPos, + args, + enumType + ); + + // Create MethodCallInfo for constructor validation if args present + MethodCallInfo constructorCall = null; + if (argsClause != null && !argsClause.isEmpty()) { + int openParenPos = bodyOffset + m.start(2); + int closeParenPos = bodyOffset + m.end(2) - 1; + + constructorCall = createConstructorCall( + enumType, + constantName, + absPos, + openParenPos, + closeParenPos + ); + } else if (enumType.hasConstructors()) { + // No args provided, but enum has constructors - validate against no-arg constructor + int openParenPos = absPos + constantName.length(); + int closeParenPos = openParenPos; + + constructorCall = createConstructorCall( + enumType, + constantName, + absPos, + openParenPos, + closeParenPos + ); + } + + EnumConstantInfo constantInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType); + constants.add(constantInfo); + + lastEnd = m.end(); + } + + return constants; + } + + /** + * Create a MethodCallInfo for an enum constant's constructor call. + * Validates the arguments against available constructors. + */ + private static MethodCallInfo createConstructorCall( + ScriptTypeInfo enumType, + String constantName, + int constantStart, + int openParenPos, + int closeParenPos) { + + List constructors = enumType.getConstructors(); + + // Parse arguments - pass absolute positions (bodyOffset accounts for the offset into the document) + List arguments = ScriptDocument.INSTANCE.parseMethodArguments( + openParenPos + 1, + closeParenPos, + null + ); + + // Find matching constructor + MethodInfo matchedConstructor = null; + List candidates = new ArrayList<>(); + + for (MethodInfo constructor : constructors) { + candidates.add(constructor); + if (constructor.getParameterCount() == arguments.size()) { + matchedConstructor = constructor; + break; + } + } + + // Create MethodCallInfo + MethodCallInfo callInfo = new MethodCallInfo( + constantName, // Use constant name as "method" name + constantStart, + openParenPos, + openParenPos, + closeParenPos, + arguments, + enumType, // Receiver is the enum type + matchedConstructor + ); + + + callInfo.setConstructor(true); + callInfo.validate(); + return callInfo; + } + + + /** + * Find where enum constants section ends. + * Returns the position of the first semicolon that's not inside parentheses, + * or the position of the first method/field declaration. + */ + private static int findEnumConstantsEnd(String bodyText) { + int parenDepth = 0; + boolean foundConstant = false; + + for (int i = 0; i < bodyText.length(); i++) { + char c = bodyText.charAt(i); + + if (c == '(') { + parenDepth++; + } else if (c == ')') { + parenDepth--; + } else if (c == ';' && parenDepth == 0) { + return i; + } else if (c == '{' && parenDepth == 0) { + // Start of method body - constants end before this + return findStatementStart(bodyText, i); + } + + // Track if we've found at least one identifier (potential constant) + if (Character.isJavaIdentifierStart(c)) { + foundConstant = true; + } + } + + // If we found constants but no semicolon, return the full length + return foundConstant ? bodyText.length() : -1; + } + + /** + * Find the start of a statement by going backwards from a position. + */ + private static int findStatementStart(String text, int fromPos) { + int pos = fromPos - 1; + int parenDepth = 0; + + while (pos >= 0) { + char c = text.charAt(pos); + if (c == ')') + parenDepth++; + else if (c == '(') + parenDepth--; + else if ((c == ',' || c == ';' || c == '{' || c == '}') && parenDepth == 0) { + return pos + 1; + } + pos--; + } + return 0; + } + + // ==================== GETTERS ==================== + + public FieldInfo getFieldInfo() { + return fieldInfo; + } + + public MethodCallInfo getConstructorCall() { + return constructorCall; + } + + public ScriptTypeInfo getEnumType() { + return enumType; + } + + public String getName() { + return fieldInfo.getName(); + } + + public int getDeclarationOffset() { + return fieldInfo.getDeclarationOffset(); + } + + /** + * Check if this enum constant has any errors (constructor mismatch, etc). + */ + public boolean hasError() { + return (constructorCall != null && constructorCall.hasError()); + } + + /** + * Get the error message if any. + */ + public String getErrorMessage() { + return null; + } +} From 75c5637aa79dc5fdbcfd59fece4969c1bdaa36c5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 16:10:45 +0200 Subject: [PATCH 133/337] Added FieldInfo initStart/initEnd for enum constants --- .../interpreter/field/EnumConstantInfo.java | 43 +++++++++++-------- .../script/interpreter/field/FieldInfo.java | 6 ++- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java index 68b38c37f..3471d7ed5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java @@ -89,42 +89,49 @@ public static List parseEnumConstants( continue; } - // Create FieldInfo for the enum constant - FieldInfo fieldInfo = FieldInfo.enumConstant( - constantName, - enumType, - absPos, - args, - enumType - ); - + // Determine init range (the constructor arguments) + int initStart = -1; + int initEnd = -1; + // Create MethodCallInfo for constructor validation if args present MethodCallInfo constructorCall = null; if (argsClause != null && !argsClause.isEmpty()) { - int openParenPos = bodyOffset + m.start(2); - int closeParenPos = bodyOffset + m.end(2) - 1; - + initStart = bodyOffset + m.start(2); // Position of '(' + initEnd = bodyOffset + m.end(2) - 1; // Position after ')' + constructorCall = createConstructorCall( enumType, constantName, absPos, - openParenPos, - closeParenPos + initStart, + initEnd ); } else if (enumType.hasConstructors()) { // No args provided, but enum has constructors - validate against no-arg constructor - int openParenPos = absPos + constantName.length(); - int closeParenPos = openParenPos; + initStart = absPos + constantName.length(); + initEnd = initStart; constructorCall = createConstructorCall( enumType, constantName, absPos, - openParenPos, - closeParenPos + initStart, + initEnd ); } + + // Create FieldInfo for the enum constant + FieldInfo fieldInfo = FieldInfo.enumConstant( + constantName, + enumType, + absPos, + args, + enumType, + initStart, + initEnd + ); + EnumConstantInfo constantInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType); constants.add(constantInfo); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 660dad234..d745b2d96 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -155,11 +155,13 @@ public static FieldInfo fromReflection(Field field, TypeInfo containingType) { * @param declOffset The declaration position * @param args The constructor arguments (or null if no args) * @param containingEnum The enum type this constant belongs to + * @param initStart The position of '(' if args present, else -1 + * @param initEnd The position after ')' if args present, else -1 */ - public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, String args, TypeInfo containingEnum) { + public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, String args, TypeInfo containingEnum, int initStart, int initEnd) { // Enum constants are implicitly public static final int modifiers = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL; - return new FieldInfo(name, Scope.ENUM_CONSTANT, type, declOffset, true, null, null, -1, -1, + return new FieldInfo(name, Scope.ENUM_CONSTANT, type, declOffset, true, null, null, initStart, initEnd, modifiers, null, args, containingEnum); } From d3a1535a31e535e3395e0117fd3dead6a04ca85c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 16:11:08 +0200 Subject: [PATCH 134/337] EnumConstantInfo as token metadata --- .../gui/util/script/interpreter/ScriptLine.java | 8 ++++++++ .../gui/util/script/interpreter/token/Token.java | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 57a13ed78..7c4f73151 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -190,6 +191,13 @@ public void applyTokenMetadata(Token token, Object metadata) { FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) metadata; token.setFieldInfo(ctx.fieldInfo); token.setMethodCallInfo(ctx.methodCallInfo); + } else if (metadata instanceof EnumConstantInfo) { + EnumConstantInfo enumInfo = (EnumConstantInfo) metadata; + token.setEnumConstantInfo(enumInfo); + token.setFieldInfo(enumInfo.getFieldInfo()); + if (enumInfo.getConstructorCall() != null) + token.setMethodCallInfo(enumInfo.getConstructorCall()); + } else if (metadata instanceof FieldInfo) { token.setFieldInfo((FieldInfo) metadata); } else if (metadata instanceof MethodInfo) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index 35f956f54..60ead2493 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -3,6 +3,7 @@ import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -32,6 +33,7 @@ public class Token { private MethodCallInfo methodCallInfo; // For method calls with argument info private ImportData importData; // For import statements private FieldAccessInfo fieldAccessInfo; //For fields like Minecraft.getMinecraft.thePlayer + private EnumConstantInfo enumConstantInfo; // For enum constant references private TokenErrorMessage errorMessage; @@ -110,6 +112,15 @@ public static Token parameter(String text, int start, int end, FieldInfo info) { return t; } + public static Token enumConstant(String text, int start, int end, EnumConstantInfo info) { + Token t = new Token(text, start, end, TokenType.ENUM_CONSTANT); + t.enumConstantInfo = info; + if (info != null) { + t.fieldInfo = info.getFieldInfo(); + } + return t; + } + public static Token undefined(String text, int start, int end) { return new Token(text, start, end, TokenType.UNDEFINED_VAR); } @@ -127,6 +138,7 @@ public static Token undefined(String text, int start, int end) { public MethodCallInfo getMethodCallInfo() { return methodCallInfo; } public FieldAccessInfo getFieldAccessInfo() { return fieldAccessInfo; } public ImportData getImportData() { return importData; } + public EnumConstantInfo getEnumConstantInfo() { return enumConstantInfo; } public TokenErrorMessage getErrorMessage() { return errorMessage; } public ScriptLine getParentLine() { return parentLine; } @@ -139,6 +151,7 @@ public static Token undefined(String text, int start, int end) { public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } + public void setEnumConstantInfo(EnumConstantInfo info) { this.enumConstantInfo = info; } public void setErrorMessage(TokenErrorMessage message) { this.errorMessage = message; } public void setParentLine(ScriptLine line) { this.parentLine = line; } From aaaeaf4b19d11d7e17ddf4ff6ea87163acdecc54 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 16:11:49 +0200 Subject: [PATCH 135/337] Added hover info for enum constants --- .../interpreter/hover/TokenHoverInfo.java | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 246ab8a55..9f8f85659 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -120,7 +120,10 @@ public static TokenHoverInfo fromToken(Token token) { case METHOD_DECL: info.extractMethodDeclInfo(token); break; - + + case ENUM_CONSTANT: + info.extractEnumConstantInfo(token); + break; case GLOBAL_FIELD: info.extractGlobalFieldInfo(token); break; @@ -773,6 +776,47 @@ private void extractGlobalFieldInfo(Token token) { addInitializationTokens(token, fieldInfo); } + private void extractEnumConstantInfo(Token token) { + EnumConstantInfo enumInfo = token.getEnumConstantInfo(); + if (enumInfo == null) + return; + + FieldInfo fieldInfo = enumInfo.getFieldInfo(); + if (fieldInfo == null) + return; + + iconIndicator = "e"; + + // Add documentation if available + if (fieldInfo.getDocumentation() != null && !fieldInfo.getDocumentation().isEmpty()) { + String[] docLines = fieldInfo.getDocumentation().split("\n"); + for (String line : docLines) { + documentation.add(line); + } + } + + TypeInfo enumType = enumInfo.getEnumType(); + + if (enumType != null) { + // Show enum's package + String pkg = getPackageName(enumType); + if (pkg != null && !pkg.isEmpty()) + packageName = pkg; + + + // Type (enum type name) + int typeColor = getColorForTypeInfo(enumType); + addSegment(enumType.getSimpleName(), typeColor); + addSegment(" ", TokenType.DEFAULT.getHexColor()); + } + + // Enum constant name + addSegment(fieldInfo.getName(), TokenType.ENUM_CONSTANT.getHexColor()); + + // Add constructor arguments if available + addInitializationTokens(token, fieldInfo); + } + private void extractLocalFieldInfo(Token token) { FieldInfo fieldInfo = token.getFieldInfo(); if (fieldInfo == null) return; @@ -803,6 +847,24 @@ private void extractLocalFieldInfo(Token token) { additionalInfo.add("Local variable"); } + public String getPackageName(TypeInfo type) { + if (type == null) + return null; + + String fullName = type.getFullName(); + if (fullName != null && !fullName.isEmpty()) { + return fullName; + } else { + String pkg = type.getPackageName(); + String className = type.getSimpleName(); + if (pkg != null && !pkg.isEmpty()) { + return pkg + "." + className; + } else { + return className; + } + } + } + private void extractParameterInfo(Token token) { FieldInfo fieldInfo = token.getFieldInfo(); if (fieldInfo == null) return; From 8d2d08b8cbee9b34d875d1ec859145e1eaaa7057 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 16:14:55 +0200 Subject: [PATCH 136/337] Unified package name fetching across TokenHoverInfo extractors --- .../interpreter/hover/TokenHoverInfo.java | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 9f8f85659..033ce99b6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -665,18 +665,9 @@ private void extractMethodCallInfo(Token token) { if (containingType != null) { // Show full qualified class name (like IntelliJ) - String fullName = containingType.getFullName(); - if (fullName != null && !fullName.isEmpty()) { - packageName = fullName; - } else { - String pkg = containingType.getPackageName(); - String className = containingType.getSimpleName(); - if (pkg != null && !pkg.isEmpty()) { - packageName = pkg + "." + className; - } else { - packageName = className; - } - } + String pkg = getPackageName(containingType); + if (pkg != null && !pkg.isEmpty()) + packageName = pkg; } // Try to get actual Java method for more details @@ -752,13 +743,9 @@ private void extractGlobalFieldInfo(Token token) { // } if (declaredType != null) { - // Show field's type package.ClassName for context - String pkg = declaredType.getPackageName(); - - // For Minecraft.getMinecraft.thePlayer - // Return net.minecraft.client.Minecraft - if (accessInfo != null) - pkg = accessInfo.getReceiverType().getFullName(); + // accessInfo For Minecraft.getMinecraft.thePlayer + // Return net.minecraft.client.Minecraft, and not net.minecraft.entity.EntityPlayer + String pkg = getPackageName(accessInfo != null ? accessInfo.getReceiverType() : declaredType); if (pkg != null && !pkg.isEmpty()) packageName = pkg; From 369b34609357544809215e2360d763886a3cffc2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 16:46:07 +0200 Subject: [PATCH 137/337] Removed if (getErrors().stream().anyMatch(err -> err.contains("Cannot resolve symbol --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 033ce99b6..3d952c818 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -1230,8 +1230,6 @@ private boolean isOperatorChar(char c) { public String getIconIndicator() { return iconIndicator; } public List getDeclaration() { - if (getErrors().stream().anyMatch(err -> err.contains("Cannot resolve symbol"))) - return new ArrayList<>(); return declaration; } public List getDocumentation() { return documentation; } From fd160e980a9215a0bce043e985170b8e546e0026 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 17:13:06 +0200 Subject: [PATCH 138/337] Stored EnumConstantInfo in FieldInfo --- .../interpreter/field/EnumConstantInfo.java | 3 ++ .../interpreter/field/FieldAccessInfo.java | 4 ++ .../script/interpreter/field/FieldInfo.java | 50 ++++++------------- 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java index 3471d7ed5..776d2900e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java @@ -6,6 +6,8 @@ import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -133,6 +135,7 @@ public static List parseEnumConstants( ); EnumConstantInfo constantInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType); + fieldInfo.setEnumConstantInfo(constantInfo); constants.add(constantInfo); lastEnd = m.end(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java index 3e0ae760d..dc29943e3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java @@ -65,6 +65,10 @@ public boolean isStaticAccess() { return isStaticAccess; } + public boolean isEnumConstantAccess() { + return resolvedField != null && resolvedField.isEnumConstant(); + } + public TypeInfo getExpectedType() { return expectedType; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index d745b2d96..a8e6a1ce0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -50,17 +50,14 @@ public enum Scope { // Declaration assignment (for initial value validation) private AssignmentInfo declarationAssignment; - - // For enum constants: the arguments passed to the constructor (null if no args or not enum constant) - private final String enumConstantArgs; - - // For enum constants: the containing enum type - private final TypeInfo containingEnumType; + // Enum constant specific info + private EnumConstantInfo enumConstantInfo; + private FieldInfo(String name, Scope scope, TypeInfo declaredType, int declarationOffset, boolean resolved, MethodInfo containingMethod, String documentation, int initStart, int initEnd, int modifiers, - Field reflectionField, String enumConstantArgs, TypeInfo containingEnumType) { + Field reflectionField) { this.name = name; this.scope = scope; this.declaredType = declaredType; @@ -72,17 +69,6 @@ private FieldInfo(String name, Scope scope, TypeInfo declaredType, this.initEnd = initEnd; this.modifiers = modifiers; this.reflectionField = reflectionField; - this.enumConstantArgs = enumConstantArgs; - this.containingEnumType = containingEnumType; - } - - // Private constructor for backward compatibility - private FieldInfo(String name, Scope scope, TypeInfo declaredType, - int declarationOffset, boolean resolved, MethodInfo containingMethod, - String documentation, int initStart, int initEnd, int modifiers, - Field reflectionField) { - this(name, scope, declaredType, declarationOffset, resolved, containingMethod, - documentation, initStart, initEnd, modifiers, reflectionField, null, null); } // Factory methods @@ -162,11 +148,18 @@ public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, // Enum constants are implicitly public static final int modifiers = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL; return new FieldInfo(name, Scope.ENUM_CONSTANT, type, declOffset, true, null, null, initStart, initEnd, - modifiers, null, args, containingEnum); + modifiers, null); + } + + // ==================== ENUM HANDLING ==================== + + public void setEnumConstantInfo(EnumConstantInfo enumConstantInfo) { + this.enumConstantInfo = enumConstantInfo; + } + + public EnumConstantInfo getEnumInfo() { + return enumConstantInfo; } - - // ==================== ERROR HANDLING ==================== - /** * Check if this is an enum constant. */ @@ -174,19 +167,6 @@ public boolean isEnumConstant() { return scope == Scope.ENUM_CONSTANT; } - /** - * Get the enum constant args (for enum constants only). - */ - public String getEnumConstantArgs() { - return enumConstantArgs; - } - - /** - * Get the containing enum type (for enum constants only). - */ - public TypeInfo getContainingEnumType() { - return containingEnumType; - } // ==================== ASSIGNMENT MANAGEMENT ==================== From 5553ece3e630b6eecf320a1fdcc3ed1b70d55a8d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 20:37:47 +0200 Subject: [PATCH 139/337] Processed external/java class enums properly --- .../interpreter/field/EnumConstantInfo.java | 74 +++++++++++++++++-- .../script/interpreter/field/FieldInfo.java | 15 +++- .../script/interpreter/type/TypeInfo.java | 32 ++++++++ 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java index 776d2900e..f99ad210a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java @@ -22,9 +22,9 @@ public class EnumConstantInfo { private final FieldInfo fieldInfo; private final MethodCallInfo constructorCall; // Can be null if no args provided - private final ScriptTypeInfo enumType; + private final TypeInfo enumType; - private EnumConstantInfo(FieldInfo fieldInfo, MethodCallInfo constructorCall, ScriptTypeInfo enumType) { + public EnumConstantInfo(FieldInfo fieldInfo, MethodCallInfo constructorCall, TypeInfo enumType) { this.fieldInfo = fieldInfo; this.constructorCall = constructorCall; this.enumType = enumType; @@ -128,10 +128,9 @@ public static List parseEnumConstants( constantName, enumType, absPos, - args, - enumType, initStart, - initEnd + initEnd, + null ); EnumConstantInfo constantInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType); @@ -193,7 +192,68 @@ private static MethodCallInfo createConstructorCall( callInfo.validate(); return callInfo; } - + + /** + * Create EnumConstantInfo from a reflection-based enum constant. + * Used for enum constants from external libraries (via reflection). + * + * @param constantName The name of the enum constant + * @param enumType The enum type this constant belongs to + * @return EnumConstantInfo representing this constant, or null if not found + */ + public static EnumConstantInfo fromReflection(String constantName, TypeInfo enumType, Field javaField) { + if (enumType == null || enumType.getJavaClass() == null || !enumType.getJavaClass().isEnum()) { + return null; + } + + try { + Class enumClass = enumType.getJavaClass(); + + + // Create FieldInfo from the reflection field + FieldInfo fieldInfo = FieldInfo.enumConstant( + constantName, + enumType, + -1, // No declaration offset for reflection-based constants + -1, + -1, + javaField + ); + + // Create a MethodCallInfo for the constructor validation + // Get constructors from the Java enum class itself + Constructor[] javaConstructors = enumClass.getDeclaredConstructors(); + Constructor matchedConstructor = null; + + // Find the primary constructor (usually the first one) + if (javaConstructors.length > 0) { + matchedConstructor = javaConstructors[0]; + } + + // Create MethodCallInfo for the constructor call + // For reflection-based enums, we don't have actual position info, so use -1 + MethodCallInfo constructorCall = null; + if (matchedConstructor != null) { + constructorCall = new MethodCallInfo( + constantName, // Method name + -1, // No position info for reflection + -1, // No paren position + -1, // No open paren position + -1, // No close paren position + new ArrayList<>(), // Empty arguments (reflection doesn't give us this) + enumType, // Receiver is the enum type + null // No MethodInfo for reflection-based constructor + ); + constructorCall.setConstructor(true); + } + EnumConstantInfo enumInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType); + fieldInfo.setEnumConstantInfo(enumInfo); + return enumInfo; + } catch (SecurityException e) { + // Constant not found or access denied + return null; + } + } /** * Find where enum constants section ends. @@ -259,7 +319,7 @@ public MethodCallInfo getConstructorCall() { return constructorCall; } - public ScriptTypeInfo getEnumType() { + public TypeInfo getEnumType() { return enumType; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index a8e6a1ce0..fd6890fb0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -131,6 +131,15 @@ public static FieldInfo reflectionParam(String name, TypeInfo type) { public static FieldInfo fromReflection(Field field, TypeInfo containingType) { String name = field.getName(); TypeInfo type = TypeInfo.fromClass(field.getType()); + + // Check if this is an enum constant + if (field.isEnumConstant()) { + // Create enum constant FieldInfo + EnumConstantInfo constantInfo = EnumConstantInfo.fromReflection(name, containingType,field); + if (constantInfo != null) + return constantInfo.getFieldInfo(); + } + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1, field.getModifiers(), field); } @@ -139,16 +148,14 @@ public static FieldInfo fromReflection(Field field, TypeInfo containingType) { * @param name The constant name (e.g., "NORTH") * @param type The enum type itself * @param declOffset The declaration position - * @param args The constructor arguments (or null if no args) - * @param containingEnum The enum type this constant belongs to * @param initStart The position of '(' if args present, else -1 * @param initEnd The position after ')' if args present, else -1 */ - public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, String args, TypeInfo containingEnum, int initStart, int initEnd) { + public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, int initStart, int initEnd,Field javaField) { // Enum constants are implicitly public static final int modifiers = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL; return new FieldInfo(name, Scope.ENUM_CONSTANT, type, declOffset, true, null, null, initStart, initEnd, - modifiers, null); + modifiers, javaField); } // ==================== ENUM HANDLING ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 2d9d97252..0d389c93f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter.type; +import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; @@ -283,6 +284,37 @@ public boolean hasField(String fieldName) { return false; } + public boolean hasEnumConstant(String constantName) { + if (javaClass == null || !javaClass.isEnum()) return false; + try { + Object[] constants = javaClass.getEnumConstants(); + for (Object constant : constants) { + if (constant.toString().equals(constantName)) + return true; + } + } catch (Exception e) { + // Security or linkage error + } + return false; + } + + /** + * Get an enum constant by name. + */ + public EnumConstantInfo getEnumConstant(String constantName) { + if (javaClass == null || !javaClass.isEnum()) return null; + try { + Object[] constants = javaClass.getEnumConstants(); + for (Object constant : constants) { + if (constant.toString().equals(constantName)) + return EnumConstantInfo.fromReflection(constantName, this, null); + } + } catch (Exception e) { + // Security or linkage error + } + return null; + } + /** * Get MethodInfo for a method by name. Returns null if not found. * Creates a synthetic MethodInfo based on reflection data. From ca11c3c9f6e9dbf393076fd1690916db4d75a5e2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 20:38:18 +0200 Subject: [PATCH 140/337] Added script enums to ScriptType's field pool --- .../interpreter/type/ScriptTypeInfo.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index dece8ba1e..eaeaf015f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -189,12 +189,27 @@ public void addField(FieldInfo field) { @Override public boolean hasField(String fieldName) { - return fields.containsKey(fieldName); + if (fields.containsKey(fieldName)) + return true; + + if (enumConstants.containsKey(fieldName)) + return true; + + return false; } @Override public FieldInfo getFieldInfo(String fieldName) { - return fields.get(fieldName); + if (fields.containsKey(fieldName)) + return fields.get(fieldName); + + if (enumConstants.containsKey(fieldName)) { + EnumConstantInfo enumConst = enumConstants.get(fieldName); + if (enumConst != null) + return enumConst.getFieldInfo(); + } + + return null; } public Map getFields() { From 5e13f580bba4885a6cc9f418a256138330dd9d08 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 20:38:50 +0200 Subject: [PATCH 141/337] Removed token EnumConstantInfo as no longer needed --- .../gui/util/script/interpreter/ScriptLine.java | 11 +++-------- .../script/interpreter/hover/TokenHoverInfo.java | 10 +++++----- .../gui/util/script/interpreter/token/Token.java | 13 ------------- 3 files changed, 8 insertions(+), 26 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java index 7c4f73151..f3deb5ecf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/ScriptLine.java @@ -191,13 +191,6 @@ public void applyTokenMetadata(Token token, Object metadata) { FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) metadata; token.setFieldInfo(ctx.fieldInfo); token.setMethodCallInfo(ctx.methodCallInfo); - } else if (metadata instanceof EnumConstantInfo) { - EnumConstantInfo enumInfo = (EnumConstantInfo) metadata; - token.setEnumConstantInfo(enumInfo); - token.setFieldInfo(enumInfo.getFieldInfo()); - if (enumInfo.getConstructorCall() != null) - token.setMethodCallInfo(enumInfo.getConstructorCall()); - } else if (metadata instanceof FieldInfo) { token.setFieldInfo((FieldInfo) metadata); } else if (metadata instanceof MethodInfo) { @@ -205,7 +198,9 @@ public void applyTokenMetadata(Token token, Object metadata) { } else if (metadata instanceof ImportData) { token.setImportData((ImportData) metadata); } else if (metadata instanceof FieldAccessInfo) { - token.setFieldAccessInfo((FieldAccessInfo) metadata); + FieldAccessInfo accessInfo = (FieldAccessInfo) metadata; + token.setFieldAccessInfo(accessInfo); + token.setFieldInfo(accessInfo.getResolvedField()); } else if (metadata instanceof TokenErrorMessage) { token.setErrorMessage((TokenErrorMessage) metadata); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 3d952c818..9f9fae12d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -764,13 +764,13 @@ private void extractGlobalFieldInfo(Token token) { } private void extractEnumConstantInfo(Token token) { - EnumConstantInfo enumInfo = token.getEnumConstantInfo(); - if (enumInfo == null) - return; - - FieldInfo fieldInfo = enumInfo.getFieldInfo(); + FieldInfo fieldInfo = token.getFieldInfo(); if (fieldInfo == null) return; + + EnumConstantInfo enumInfo = fieldInfo.getEnumInfo(); + if (enumInfo == null) + return; iconIndicator = "e"; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index 60ead2493..194878c76 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -33,8 +33,6 @@ public class Token { private MethodCallInfo methodCallInfo; // For method calls with argument info private ImportData importData; // For import statements private FieldAccessInfo fieldAccessInfo; //For fields like Minecraft.getMinecraft.thePlayer - private EnumConstantInfo enumConstantInfo; // For enum constant references - private TokenErrorMessage errorMessage; @@ -112,15 +110,6 @@ public static Token parameter(String text, int start, int end, FieldInfo info) { return t; } - public static Token enumConstant(String text, int start, int end, EnumConstantInfo info) { - Token t = new Token(text, start, end, TokenType.ENUM_CONSTANT); - t.enumConstantInfo = info; - if (info != null) { - t.fieldInfo = info.getFieldInfo(); - } - return t; - } - public static Token undefined(String text, int start, int end) { return new Token(text, start, end, TokenType.UNDEFINED_VAR); } @@ -138,7 +127,6 @@ public static Token undefined(String text, int start, int end) { public MethodCallInfo getMethodCallInfo() { return methodCallInfo; } public FieldAccessInfo getFieldAccessInfo() { return fieldAccessInfo; } public ImportData getImportData() { return importData; } - public EnumConstantInfo getEnumConstantInfo() { return enumConstantInfo; } public TokenErrorMessage getErrorMessage() { return errorMessage; } public ScriptLine getParentLine() { return parentLine; } @@ -151,7 +139,6 @@ public static Token undefined(String text, int start, int end) { public void setMethodInfo(MethodInfo info) { this.methodInfo = info; } public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; } public void setImportData(ImportData data) { this.importData = data; } - public void setEnumConstantInfo(EnumConstantInfo info) { this.enumConstantInfo = info; } public void setErrorMessage(TokenErrorMessage message) { this.errorMessage = message; } public void setParentLine(ScriptLine line) { this.parentLine = line; } From fbbd53d5ff5b55d4a17844f5e45ad94d55b5953b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 20:56:47 +0200 Subject: [PATCH 142/337] Removed unneeded enum constructor creation --- .../interpreter/field/EnumConstantInfo.java | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java index f99ad210a..f86b3e146 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/EnumConstantInfo.java @@ -207,10 +207,6 @@ public static EnumConstantInfo fromReflection(String constantName, TypeInfo enum } try { - Class enumClass = enumType.getJavaClass(); - - - // Create FieldInfo from the reflection field FieldInfo fieldInfo = FieldInfo.enumConstant( constantName, enumType, @@ -220,33 +216,7 @@ public static EnumConstantInfo fromReflection(String constantName, TypeInfo enum javaField ); - // Create a MethodCallInfo for the constructor validation - // Get constructors from the Java enum class itself - Constructor[] javaConstructors = enumClass.getDeclaredConstructors(); - Constructor matchedConstructor = null; - - // Find the primary constructor (usually the first one) - if (javaConstructors.length > 0) { - matchedConstructor = javaConstructors[0]; - } - - // Create MethodCallInfo for the constructor call - // For reflection-based enums, we don't have actual position info, so use -1 - MethodCallInfo constructorCall = null; - if (matchedConstructor != null) { - constructorCall = new MethodCallInfo( - constantName, // Method name - -1, // No position info for reflection - -1, // No paren position - -1, // No open paren position - -1, // No close paren position - new ArrayList<>(), // Empty arguments (reflection doesn't give us this) - enumType, // Receiver is the enum type - null // No MethodInfo for reflection-based constructor - ); - constructorCall.setConstructor(true); - } - EnumConstantInfo enumInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType); + EnumConstantInfo enumInfo = new EnumConstantInfo(fieldInfo, null, enumType); fieldInfo.setEnumConstantInfo(enumInfo); return enumInfo; } catch (SecurityException e) { From 4e0c5e91da240184cad26c72c5828e64f570a477 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 21:24:45 +0200 Subject: [PATCH 143/337] Cleaned up markChainedAccessFields heavily --- .../script/interpreter/FieldChainMarker.java | 443 ++++++++++++++++++ .../script/interpreter/ScriptDocument.java | 421 +---------------- 2 files changed, 461 insertions(+), 403 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java new file mode 100644 index 000000000..8e28a4021 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java @@ -0,0 +1,443 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper class for marking chained field accesses in script documents. + * Handles patterns like: identifier.field, this.field, super.field, method().field + */ +public class FieldChainMarker { + private final ScriptDocument document; + private final String text; + + public FieldChainMarker(ScriptDocument document, String text) { + this.document = document; + this.text = text; + } + + /** + * Mark all chained field accesses in the document. + * Uses two passes: one for standard chains, one for method/array result chains. + */ + public void markChainedFieldAccesses(List marks) { + // ==================== PASS 1: Standard identifier chains ==================== + // Pattern: identifier.identifier, this.identifier, super.identifier + Pattern chainPattern = Pattern.compile( + "\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher m = chainPattern.matcher(text); + + while (m.find()) { + int chainStart = m.start(1); + + // Skip excluded ranges and imports + if (document.isExcluded(chainStart) || document.isInImportOrPackage(chainStart)) continue; + + // Skip if second segment is a method call - markMethodCalls handles it + if (document.isFollowedByParen(m.end(2))) continue; + + // Build the full chain + ChainData chain = buildChain(m); + if (chain == null) continue; + + // Resolve the starting type + ChainContext ctx = resolveChainStart(chain, chainStart); + + // Mark each segment + for (int i = ctx.startIndex; i < chain.segments.size(); i++) { + String segment = chain.segments.get(i); + int[] pos = chain.positions.get(i); + + if (document.isExcluded(pos[0])) continue; + + boolean isLast = (i == chain.segments.size() - 1); + boolean isStatic = (i > 0) && isStaticContext(chain.segments.get(i - 1)); + + // Determine what type of marking this segment needs + MarkResult result = resolveSegmentMark(ctx, segment, pos, i, isLast, isStatic, chainStart); + + // Apply the mark + if (result.mark != null) { + marks.add(result.mark); + } + + // Update context for next segment + ctx.currentType = result.nextType; + } + } + + // ==================== PASS 2: Method/array result chains ==================== + // Handle: getMinecraft().thePlayer, array[0].length + Pattern dotIdent = Pattern.compile("\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher md = dotIdent.matcher(text); + + while (md.find()) { + int identStart = md.start(1); + int identEnd = md.end(1); + int dotPos = md.start(); + + // Skip excluded ranges and imports + if (document.isExcluded(identStart) || document.isInImportOrPackage(identStart)) continue; + + // Only handle when preceded by ')' or ']' (method call or array access result) + Character precedingChar = getNonWhitespaceBefore(dotPos); + if (precedingChar == null || (precedingChar != ')' && precedingChar != ']')) continue; + + // Get receiver type from the expression before the dot + int[] bounds = document.findReceiverBoundsBefore(dotPos); + if (bounds == null) continue; + + if (document.isExcluded(bounds[0]) || document.isInImportOrPackage(bounds[0])) continue; + + String receiverExpr = text.substring(bounds[0], bounds[1]).trim(); + if (receiverExpr.isEmpty()) continue; + + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, bounds[0]); + + // Mark the first field and continue the chain + markReceiverChainSegments(marks, md.group(1), identStart, identEnd, receiverType); + } + } + + // ==================== CHAIN BUILDING HELPERS ==================== + + /** Data class for a parsed chain */ + private static class ChainData { + final List segments = new ArrayList<>(); + final List positions = new ArrayList<>(); + } + + /** Context for chain resolution */ + private static class ChainContext { + TypeInfo currentType; + int startIndex; + boolean firstIsThis; + boolean firstIsSuper; + boolean firstIsPrecededByDot; + } + + /** Result of resolving a segment's mark */ + private static class MarkResult { + final ScriptLine.Mark mark; + final TypeInfo nextType; + + MarkResult(ScriptLine.Mark mark, TypeInfo nextType) { + this.mark = mark; + this.nextType = nextType; + } + } + + /** Build a complete chain from a regex match, continuing to read more segments */ + private ChainData buildChain(Matcher m) { + ChainData chain = new ChainData(); + + // Add first two segments from the match + chain.segments.add(m.group(1)); + chain.positions.add(new int[]{m.start(1), m.end(1)}); + chain.segments.add(m.group(2)); + chain.positions.add(new int[]{m.start(2), m.end(2)}); + + // Continue reading subsequent .identifier segments + int pos = m.end(2); + while (pos < text.length()) { + pos = document.skipWhitespace(pos); + if (pos >= text.length() || text.charAt(pos) != '.') break; + pos++; // Skip dot + + pos = document.skipWhitespace(pos); + if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break; + + // Read identifier + int identStart = pos; + while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++; + int identEnd = pos; + + // Stop if this is a method call + if (document.isFollowedByParen(identEnd)) break; + + chain.segments.add(text.substring(identStart, identEnd)); + chain.positions.add(new int[]{identStart, identEnd}); + } + + return chain; + } + + /** Resolve the starting context for a chain */ + private ChainContext resolveChainStart(ChainData chain, int chainStart) { + ChainContext ctx = new ChainContext(); + String first = chain.segments.get(0); + + ctx.firstIsThis = first.equals("this"); + ctx.firstIsSuper = first.equals("super"); + ctx.firstIsPrecededByDot = document.isPrecededByDot(chainStart); + ctx.startIndex = ctx.firstIsPrecededByDot ? 0 : 1; + ctx.currentType = null; + + if (ctx.firstIsThis) { + // 'this' - will use globalFields for resolution + ctx.currentType = null; + } else if (ctx.firstIsSuper) { + // 'super' - resolve to parent class + ScriptTypeInfo enclosingType = document.findEnclosingScriptType(chainStart); + if (enclosingType != null && enclosingType.hasSuperClass()) { + ctx.currentType = enclosingType.getSuperClass(); + } + } else if (ctx.firstIsPrecededByDot) { + // Field access on a receiver (e.g., getMinecraft().thePlayer) + TypeInfo receiverType = document.resolveReceiverChain(chainStart); + if (receiverType != null && receiverType.hasField(first)) { + FieldInfo varInfo = receiverType.getFieldInfo(first); + ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; + } + } else { + // Try as type first (static access), then as variable + TypeInfo typeCheck = document.resolveType(first); + if (typeCheck != null && typeCheck.isResolved()) { + ctx.currentType = typeCheck; + } else { + FieldInfo varInfo = document.resolveVariable(first, chainStart); + ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; + } + } + + return ctx; + } + + /** Resolve what mark a segment should get */ + private MarkResult resolveSegmentMark(ChainContext ctx, String segment, int[] pos, + int index, boolean isLast, boolean isStatic, int chainStart) { + // Case 1: First segment preceded by dot (field on receiver) + if (index == 0 && ctx.firstIsPrecededByDot) { + return resolveReceiverFieldMark(segment, pos, chainStart, isLast, isStatic); + } + + // Case 2: this.field + if (index == 1 && ctx.firstIsThis) { + return resolveThisFieldMark(segment, pos); + } + + // Case 3: super.field + if (index == 1 && ctx.firstIsSuper) { + return resolveSuperFieldMark(ctx.currentType, segment, pos, isLast); + } + + // Case 4: Resolved type with field + if (ctx.currentType != null && ctx.currentType.isResolved()) { + return resolveTypedFieldMark(ctx.currentType, segment, pos, isLast, isStatic); + } + + // Case 5: Unresolved - mark as undefined + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), + null + ); + } + + // ==================== SEGMENT RESOLUTION HELPERS ==================== + + /** Resolve mark for field access on a receiver (preceded by dot) */ + private MarkResult resolveReceiverFieldMark(String segment, int[] pos, int chainStart, + boolean isLast, boolean isStatic) { + TypeInfo receiverType = document.resolveReceiverChain(chainStart); + + if (receiverType != null && receiverType.hasField(segment)) { + FieldInfo fieldInfo = receiverType.getFieldInfo(segment); + FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], + receiverType, fieldInfo, isLast, isStatic); + + TokenType tokenType = (fieldInfo != null && fieldInfo.isEnumConstant()) + ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; + + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], tokenType, accessInfo), + (fieldInfo != null) ? fieldInfo.getTypeInfo() : null + ); + } + + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), + null + ); + } + + /** Resolve mark for this.field access */ + private MarkResult resolveThisFieldMark(String segment, int[] pos) { + if (document.getGlobalFields().containsKey(segment)) { + FieldInfo fieldInfo = document.getGlobalFields().get(segment); + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.GLOBAL_FIELD, fieldInfo), + fieldInfo.getTypeInfo() + ); + } + + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), + null + ); + } + + /** Resolve mark for super.field access */ + private MarkResult resolveSuperFieldMark(TypeInfo superType, String segment, int[] pos, boolean isLast) { + if (superType == null) { + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Cannot resolve field '" + segment + "'").clearOtherErrors()), + null + ); + } + + // Search through inheritance hierarchy + boolean found = false; + FieldInfo fieldInfo = null; + + if (superType instanceof ScriptTypeInfo) { + ScriptTypeInfo scriptSuper = (ScriptTypeInfo) superType; + found = scriptSuper.hasFieldInHierarchy(segment); + if (found) fieldInfo = scriptSuper.getFieldInfoInHierarchy(segment); + } else { + found = superType.hasField(segment); + if (found) fieldInfo = superType.getFieldInfo(segment); + } + + if (found) { + FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], + superType, fieldInfo, isLast, false); + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.GLOBAL_FIELD, accessInfo), + (fieldInfo != null) ? fieldInfo.getTypeInfo() : null + ); + } + + String errorMsg = "Field '" + segment + "' not found in parent class hierarchy starting from '" + + superType.getSimpleName() + "'"; + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR, + TokenErrorMessage.from(errorMsg).clearOtherErrors()), + null + ); + } + + /** Resolve mark for typed field access */ + private MarkResult resolveTypedFieldMark(TypeInfo currentType, String segment, int[] pos, + boolean isLast, boolean isStatic) { + if (!currentType.hasField(segment)) { + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), + null + ); + } + + FieldInfo fieldInfo = currentType.getFieldInfo(segment); + + // Check for static access error + if (isStatic && fieldInfo != null && !fieldInfo.isStatic()) { + TokenErrorMessage errorMsg = TokenErrorMessage + .from("Cannot access non-static field '" + segment + "' from static context '" + + currentType.getSimpleName() + "'") + .clearOtherErrors(); + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR, errorMsg), + null + ); + } + + // Valid field access + FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], + currentType, fieldInfo, isLast, isStatic); + + TokenType tokenType = (fieldInfo != null && fieldInfo.isEnumConstant()) + ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; + + return new MarkResult( + new ScriptLine.Mark(pos[0], pos[1], tokenType, accessInfo), + (fieldInfo != null) ? fieldInfo.getTypeInfo() : null + ); + } + + /** Mark segments following a method call or array access result */ + private void markReceiverChainSegments(List marks, String firstField, + int identStart, int identEnd, TypeInfo receiverType) { + // Mark the first field + FieldInfo fInfo = null; + TypeInfo currentType = null; + + if (receiverType != null && receiverType.hasField(firstField)) { + fInfo = receiverType.getFieldInfo(firstField); + boolean hasMore = document.isFollowedByDot(identEnd); + boolean isStatic = isStaticContext(receiverType.getSimpleName()); + + FieldAccessInfo accessInfo = document.createFieldAccessInfo(firstField, identStart, identEnd, + receiverType, fInfo, !hasMore, isStatic); + + TokenType tokenType = (fInfo != null && fInfo.isEnumConstant()) + ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; + marks.add(new ScriptLine.Mark(identStart, identEnd, tokenType, accessInfo)); + currentType = (fInfo != null) ? fInfo.getTypeInfo() : null; + } else { + marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR)); + return; + } + + // Continue the chain + int pos = identEnd; + while (pos < text.length()) { + pos = document.skipWhitespace(pos); + if (pos >= text.length() || text.charAt(pos) != '.') break; + pos++; // Skip dot + + pos = document.skipWhitespace(pos); + if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break; + + // Read identifier + int nStart = pos; + while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++; + int nEnd = pos; + String seg = text.substring(nStart, nEnd); + + // Stop if method call + if (document.isFollowedByParen(nEnd)) break; + if (document.isExcluded(nStart)) break; + + boolean isLast = !document.isFollowedByDot(nEnd); + + if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) { + FieldInfo segInfo = currentType.getFieldInfo(seg); + boolean isStatic = isStaticContext(currentType.getSimpleName()); + + FieldAccessInfo accessInfo = document.createFieldAccessInfo(seg, nStart, nEnd, + currentType, segInfo, isLast, isStatic); + + TokenType tokenType = (segInfo != null && segInfo.isEnumConstant()) + ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; + marks.add(new ScriptLine.Mark(nStart, nEnd, tokenType, accessInfo)); + currentType = (segInfo != null) ? segInfo.getTypeInfo() : null; + } else { + marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR)); + break; + } + } + } + + // ==================== UTILITY HELPERS ==================== + + /** Get the non-whitespace character before a position */ + private Character getNonWhitespaceBefore(int pos) { + int before = pos - 1; + while (before >= 0 && Character.isWhitespace(text.charAt(before))) before--; + return (before >= 0) ? text.charAt(before) : null; + } + + /** Check if a name indicates static context (starts with uppercase) */ + private boolean isStaticContext(String name) { + return name != null && !name.isEmpty() && Character.isUpperCase(name.charAt(0)); + } +} 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 5c23db663..95b3b099f 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 @@ -1371,7 +1371,7 @@ private String cleanDocumentation(String comment) { return result.isEmpty() ? null : result; } - private TypeInfo resolveType(String typeName) { + TypeInfo resolveType(String typeName) { return resolveTypeAndTrackUsage(typeName); } @@ -2460,7 +2460,7 @@ private boolean hasExcessivePrecision(String numLiteral, int maxDigits) { * * This is THE method that should be used for all type resolution needs. */ - private TypeInfo resolveExpressionType(String expr, int position) { + TypeInfo resolveExpressionType(String expr, int position) { expr = expr.trim(); if (expr.isEmpty()) { @@ -2742,7 +2742,7 @@ private List parseExpressionChain(String expr) { * @param methodNameStart The start position of the method name * @return The TypeInfo of the final receiver, or null if no receiver or couldn't resolve */ - private TypeInfo resolveReceiverChain(int methodNameStart) { + TypeInfo resolveReceiverChain(int methodNameStart) { // First check if preceded by a dot int scanPos = methodNameStart - 1; while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos))) @@ -3357,7 +3357,7 @@ else if (c == openChar) { * of the receiver expression immediately to the left of the dot. The end * returned will be the dot index (exclusive). Returns null if not found or malformed. */ - private int[] findReceiverBoundsBefore(int dotIndex) { + int[] findReceiverBoundsBefore(int dotIndex) { if (dotIndex < 0 || dotIndex >= text.length() || text.charAt(dotIndex) != '.') { return null; } @@ -3368,6 +3368,12 @@ private int[] findReceiverBoundsBefore(int dotIndex) { if (pos < 0) return null; while (pos >= 0) { + // Check if we're in a comment range (but not string range) + // Skip comments to avoid picking up types from comment text + if (isInCommentRange(pos)) { + return null; + } + char c = text.charAt(pos); if (Character.isWhitespace(c)) { pos--; continue; } @@ -3406,7 +3412,7 @@ private int[] findReceiverBoundsBefore(int dotIndex) { * Find the script-defined type that contains the given position. * Used for resolving 'this' references. */ - private ScriptTypeInfo findEnclosingScriptType(int position) { + ScriptTypeInfo findEnclosingScriptType(int position) { for (ScriptTypeInfo type : scriptTypes.values()) { if (type.containsPosition(position)) { return type; @@ -3703,386 +3709,10 @@ private void markVariables(List marks) { * Does NOT mark method calls (identifiers followed by parentheses) - those are handled by markMethodCalls. */ private void markChainedFieldAccesses(List marks) { - // Pattern to find identifier chains: identifier.identifier, this.identifier, super.identifier, etc. - // Start with an identifier, 'this', or 'super' followed by at least one dot and another identifier - Pattern chainPattern = Pattern.compile( - "\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); - Matcher m = chainPattern.matcher(text); - - while (m.find()) { - int chainStart = m.start(1); - - if (isExcluded(chainStart)) - continue; - - // Skip import/package statements - if (isInImportOrPackage(chainStart)) - continue; - - // Check if the SECOND segment is a method call (followed by parentheses) - // If so, we skip this match entirely - markMethodCalls will handle it - int afterSecond = m.end(2); - int checkPos = afterSecond; - while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos))) - checkPos++; - if (checkPos < text.length() && text.charAt(checkPos) == '(') { - // This is "something.methodCall()" - skip, handled by markMethodCalls - continue; - } - - // Build the full chain by continuing to match subsequent .identifier patterns - List chainSegments = new ArrayList<>(); - List segmentPositions = new ArrayList<>(); // [start, end] for each segment - - String firstSegment = m.group(1); - chainSegments.add(firstSegment); - segmentPositions.add(new int[]{m.start(1), m.end(1)}); - - String secondSegment = m.group(2); - chainSegments.add(secondSegment); - segmentPositions.add(new int[]{m.start(2), m.end(2)}); - - // Continue reading more segments - int pos = m.end(2); - while (pos < text.length()) { - // Skip whitespace - while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) - pos++; - - if (pos >= text.length() || text.charAt(pos) != '.') - break; - pos++; // Skip the dot - - // Skip whitespace after dot - while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) - pos++; - - if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) - break; - - // Read next identifier - int identStart = pos; - while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) - pos++; - int identEnd = pos; - - // Check if this is followed by parentheses (method call - stop) - int checkPosInner = pos; - while (checkPosInner < text.length() && Character.isWhitespace(text.charAt(checkPosInner))) - checkPosInner++; - if (checkPosInner < text.length() && text.charAt(checkPosInner) == '(') { - break; // This is a method call, not a field - } - - chainSegments.add(text.substring(identStart, identEnd)); - segmentPositions.add(new int[]{identStart, identEnd}); - } - - // Now resolve the chain type by type - // Start with resolving the first segment - TypeInfo currentType = null; - boolean firstIsThis = firstSegment.equals("this"); - boolean firstIsSuper = firstSegment.equals("super"); - boolean firstIsPrecededByDot = isPrecededByDot(chainStart); - - // Determine the starting index for marking - // If the first segment is preceded by a dot, it's a field access (not a variable), so we should mark it - int startMarkingIndex = firstIsPrecededByDot ? 0 : 1; - - if (firstIsThis) { - // For 'this', we don't have a class context to resolve, but we can mark subsequent fields - // as global fields if they exist in globalFields - currentType = null; // We'll use globalFields for the next segment - } else if (firstIsSuper) { - // For 'super', resolve to parent class type - ScriptTypeInfo enclosingType = findEnclosingScriptType(chainStart); - if (enclosingType != null && enclosingType.hasSuperClass()) { - currentType = enclosingType.getSuperClass(); - if (currentType == null || !currentType.isResolved()) { - // Mark 'super' as error - parent class not resolved - marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1], - TokenType.UNDEFINED_VAR, TokenErrorMessage - .from("Cannot resolve parent class '" + enclosingType.getSuperClassName() + "'") - .clearOtherErrors())); - return; // Can't continue - } - } else { - // Mark 'super' as error - no parent class - String errorMsg = enclosingType == null ? "'super' can only be used within a class" : "Class '" + enclosingType.getSimpleName() + "' does not have a parent class"; - marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1], - TokenType.UNDEFINED_VAR, TokenErrorMessage.from(errorMsg).clearOtherErrors())); - return; // Can't continue - } - marks.add(new ScriptLine.Mark(segmentPositions.get(0)[0], segmentPositions.get(0)[1], - TokenType.IMPORT_KEYWORD, currentType)); - } else { - // Try to resolve as a type first (class/interface/enum name) - TypeInfo typeCheck = resolveType(firstSegment); - if (typeCheck != null && typeCheck.isResolved()) { - // Static field access like SomeClass.field or scriptType.field - currentType = typeCheck; - } else if (firstIsPrecededByDot) { - // This is a field access on a previous result (e.g., getMinecraft().thePlayer) - // Resolve the receiver chain to get the type of what comes before the dot - TypeInfo receiverType = resolveReceiverChain(chainStart); - if (receiverType != null && receiverType.hasField(firstSegment)) { - FieldInfo varInfo = receiverType.getFieldInfo(firstSegment); - currentType = (varInfo != null) ? varInfo.getTypeInfo() : null; - } else { - currentType = null; // Can't resolve - } - } else { - // This is a variable starting the chain - FieldInfo varInfo = resolveVariable(firstSegment, chainStart); - if (varInfo != null) { - currentType = varInfo.getTypeInfo(); - } else { - currentType = null; - } - } - } - - // Mark the segments - start from index determined above - for (int i = startMarkingIndex; i < chainSegments.size(); i++) { - String segment = chainSegments.get(i); - int[] segPos = segmentPositions.get(i); - - if (isExcluded(segPos[0])) - continue; - - boolean isLastSegment = (i == chainSegments.size() - 1); - boolean isStaticAccess = false; - if (i - 1 >= 0) { - String prev = chainSegments.get(i - 1); - // Check if previous segment is a class/type name (indicating static access) - TypeInfo prevType = resolveType(prev); - if (prevType != null && prevType.isResolved()) { - isStaticAccess = true; - } - } - - // Handle the first segment if we're marking it (i.e., when preceded by dot) - if (i == 0 && firstIsPrecededByDot) { - // This is a field access on a receiver (e.g., the "thePlayer" in "getMinecraft().thePlayer") - TypeInfo receiverType = resolveReceiverChain(chainStart); - if (receiverType != null && receiverType.hasField(segment)) { - FieldInfo fieldInfo = receiverType.getFieldInfo(segment); - FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], receiverType, fieldInfo, isLastSegment, isStaticAccess); - - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; - } else { - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); - currentType = null; - } - } else if (i == 1 && firstIsThis) { - // For "this.field", check if field exists in globalFields - if (globalFields.containsKey(segment)) { - FieldInfo fieldInfo = globalFields.get(segment); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, fieldInfo)); - currentType = fieldInfo.getTypeInfo(); - } else { - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); - currentType = null; - } - } else if (i == 1 && firstIsSuper) { - // For "super.field", currentType is already the parent class - // Search recursively through the inheritance hierarchy - boolean found = false; - FieldInfo fieldInfo = null; - - if (currentType != null) { - if (currentType instanceof ScriptTypeInfo) { - found = ((ScriptTypeInfo) currentType).hasFieldInHierarchy(segment); - if (found) { - fieldInfo = ((ScriptTypeInfo) currentType).getFieldInfoInHierarchy(segment); - } - } else { - // For Java classes, hasField already checks inheritance - found = currentType.hasField(segment); - if (found) { - fieldInfo = currentType.getFieldInfo(segment); - } - } - } - - if (found) { - FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], currentType, - fieldInfo, isLastSegment, false); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - currentType = (fieldInfo != null) ? fieldInfo.getTypeInfo() : null; - } else { - String errorMsg = currentType != null ? "Field '" + segment + "' not found in parent class hierarchy starting from '" + currentType.getSimpleName() + "'" : "Cannot resolve field '" + segment + "'"; - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, - TokenErrorMessage.from(errorMsg).clearOtherErrors())); - currentType = null; - } - } else if (currentType != null && currentType.isResolved()) { - // Check if this type has this field - if (currentType.hasField(segment)) { - FieldInfo fieldInfo = currentType.getFieldInfo(segment); - - // If accessing from a class name (isStaticAccessSeg) and field is not static, that's an error - if (isStaticAccess && fieldInfo != null && !fieldInfo.isStatic()) { - // Mark as error - trying to access non-static field from static context - TokenErrorMessage errorMsg = TokenErrorMessage - .from("Cannot access non-static field '" + segment + "' from static context '" + currentType.getSimpleName() + "'") - .clearOtherErrors(); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR, errorMsg)); - currentType = null; // Can't continue resolving - } else { - // Valid access - FieldAccessInfo accessInfo = createFieldAccessInfo(segment, segPos[0], segPos[1], - currentType, fieldInfo, isLastSegment, isStaticAccess); - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.GLOBAL_FIELD, accessInfo)); - - // Update currentType for next segment - currentType = (fieldInfo != null && fieldInfo.getTypeInfo() != null) ? fieldInfo.getTypeInfo() : null; - } - } else { - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); - currentType = null; // Can't continue resolving - } - } else { - // Can't resolve - mark as undefined - marks.add(new ScriptLine.Mark(segPos[0], segPos[1], TokenType.UNDEFINED_VAR)); - currentType = null; - } - } - } - - // Second pass: handle cases where the receiver is a method call or array access - // Example: getMinecraft().thePlayer -> initial chainPattern won't match because - // the left side isn't a simple identifier. Find ".identifier" occurrences where - // the char before the dot ends with ')' or ']' and resolve the receiver expression. - Pattern dotIdent = Pattern.compile("\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); - Matcher md = dotIdent.matcher(text); - while (md.find()) { - int identStart = md.start(1); - int identEnd = md.end(1); - int dotPos = md.start(); - - // Skip if excluded or in import/package - if (isExcluded(identStart) || isInImportOrPackage(identStart)) - continue; - - // Find non-whitespace char before the dot - int before = dotPos - 1; - while (before >= 0 && Character.isWhitespace(text.charAt(before))) - before--; - if (before < 0) - continue; - - char bc = text.charAt(before); - // Only handle when left side ends with ')' or ']' (method call or array access) - if (bc != ')' && bc != ']') - continue; - - // Extract receiver bounds and resolve its type - int[] bounds = findReceiverBoundsBefore(dotPos); - if (bounds == null) - continue; - int recvStart = bounds[0]; - int recvEnd = bounds[1]; - - if (isExcluded(recvStart) || isInImportOrPackage(recvStart)) - continue; - - String receiverExpr = text.substring(recvStart, recvEnd).trim(); - if (receiverExpr.isEmpty()) - continue; - - TypeInfo receiverType = resolveExpressionType(receiverExpr, recvStart); - - // Now mark the first field after the dot and continue chaining - String firstField = md.group(1); - FieldInfo fInfo = null; - if (receiverType != null && receiverType.hasField(firstField)) { - fInfo = receiverType.getFieldInfo(firstField); - - // Check if there are more segments after this one - boolean hasMoreSegments = false; - int checkPos = identEnd; - while (checkPos < text.length() && Character.isWhitespace(text.charAt(checkPos))) - checkPos++; - if (checkPos < text.length() && text.charAt(checkPos) == '.') { - hasMoreSegments = true; - } - - boolean isStaticAccessFirst = false; - if (receiverType != null) { - String recvName = receiverType.getSimpleName(); - if (recvName != null && !recvName.isEmpty() && Character.isUpperCase(recvName.charAt(0))) - isStaticAccessFirst = true; - } - FieldAccessInfo accessInfoFirst = createFieldAccessInfo(firstField, identStart, identEnd, receiverType, fInfo, !hasMoreSegments, isStaticAccessFirst); - marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.GLOBAL_FIELD, accessInfoFirst)); - } else { - marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR)); - } - - // Attempt to continue the chain after this identifier - int pos = identEnd; - TypeInfo currentType = (fInfo != null) ? fInfo.getTypeInfo() : null; - while (pos < text.length()) { - // Skip whitespace - while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) - pos++; - if (pos >= text.length() || text.charAt(pos) != '.') - break; - pos++; // skip dot - // Skip whitespace - while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) - pos++; - if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) - break; - - int nStart = pos; - while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) - pos++; - int nEnd = pos; - String seg = text.substring(nStart, nEnd); - - // Check if next segment is a method call (followed by '(') -> stop - int checkPosInner = nEnd; - while (checkPosInner < text.length() && Character.isWhitespace(text.charAt(checkPosInner))) - checkPosInner++; - if (checkPosInner < text.length() && text.charAt(checkPosInner) == '(') - break; - - if (isExcluded(nStart)) - break; - - // Check if this is the last segment (no more dots after) - boolean isLastSegment = true; - int checkNext = nEnd; - while (checkNext < text.length() && Character.isWhitespace(text.charAt(checkNext))) - checkNext++; - if (checkNext < text.length() && text.charAt(checkNext) == '.') { - isLastSegment = false; - } - - if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) { - FieldInfo segInfo = currentType.getFieldInfo(seg); - - boolean isStaticAccessSeg2 = false; - if (currentType != null) { - String tn = currentType.getSimpleName(); - if (tn != null && !tn.isEmpty() && Character.isUpperCase(tn.charAt(0))) - isStaticAccessSeg2 = true; - } - FieldAccessInfo accessInfoSeg = createFieldAccessInfo(seg, nStart, nEnd, currentType, segInfo, isLastSegment, isStaticAccessSeg2); - marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.GLOBAL_FIELD, accessInfoSeg)); - - currentType = (segInfo != null) ? segInfo.getTypeInfo() : null; - } else { - marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR)); - currentType = null; - } - } - } - + FieldChainMarker marker = new FieldChainMarker(this, text); + marker.markChainedFieldAccesses(marks); } + /** * Parse assignment statements (reassignments, not declarations) and validate type compatibility. @@ -4913,7 +4543,7 @@ class OpenBrace { * Helper to create and validate a FieldAccessInfo. Does NOT add marks or register the info — * callers should add to `fieldAccesses` and `marks` as appropriate so marking logic remains in-place. */ - private FieldAccessInfo createFieldAccessInfo(String name, int start, int end, + FieldAccessInfo createFieldAccessInfo(String name, int start, int end, TypeInfo receiverType, FieldInfo fieldInfo, boolean isLastSegment, boolean isStaticAccess) { FieldAccessInfo accessInfo = new FieldAccessInfo(name, start, end, receiverType, fieldInfo, isStaticAccess); @@ -4928,7 +4558,7 @@ private FieldAccessInfo createFieldAccessInfo(String name, int start, int end, return accessInfo; } - private FieldInfo resolveVariable(String name, int position) { + FieldInfo resolveVariable(String name, int position) { // Find containing method MethodInfo containingMethod = findMethodAtPosition(position); @@ -4962,7 +4592,7 @@ private FieldInfo resolveVariable(String name, int position) { return null; } - private boolean isPrecededByDot(int position) { + boolean isPrecededByDot(int position) { if (position <= 0) return false; int i = position - 1; @@ -4971,22 +4601,7 @@ private boolean isPrecededByDot(int position) { return i >= 0 && text.charAt(i) == '.'; } - private boolean isFollowedByParen(int position) { - int i = position; - while (i < text.length() && Character.isWhitespace(text.charAt(i))) - i++; - return i < text.length() && text.charAt(i) == '('; - } - - private boolean isFollowedByDot(int position) { - // Start checking after the given position (e.g. after a closing paren) - int i = position + 1; - while (i < text.length() && Character.isWhitespace(text.charAt(i))) - i++; - return i < text.length() && text.charAt(i) == '.'; - } - - private boolean isInImportOrPackage(int position) { + boolean isInImportOrPackage(int position) { if (position < 0 || position >= text.length()) return false; From 9a575919b49b9edccf7758bc1e1efd18608b099b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 21:55:04 +0200 Subject: [PATCH 144/337] Refined FieldChainMarker --- .../script/interpreter/FieldChainMarker.java | 156 +++++++++++------- 1 file changed, 99 insertions(+), 57 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java index 8e28a4021..9a3e2d181 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java @@ -54,16 +54,13 @@ public void markChainedFieldAccesses(List marks) { // Mark each segment for (int i = ctx.startIndex; i < chain.segments.size(); i++) { - String segment = chain.segments.get(i); + ctx.currentIndex = i; int[] pos = chain.positions.get(i); if (document.isExcluded(pos[0])) continue; - boolean isLast = (i == chain.segments.size() - 1); - boolean isStatic = (i > 0) && isStaticContext(chain.segments.get(i - 1)); - // Determine what type of marking this segment needs - MarkResult result = resolveSegmentMark(ctx, segment, pos, i, isLast, isStatic, chainStart); + MarkResult result = resolveSegmentMark(ctx, chainStart); // Apply the mark if (result.mark != null) { @@ -118,7 +115,10 @@ private static class ChainData { /** Context for chain resolution */ private static class ChainContext { + ChainData chain; + int currentIndex; TypeInfo currentType; + ScriptTypeInfo enclosingType; // The enclosing script type for 'this' resolution int startIndex; boolean firstIsThis; boolean firstIsSuper; @@ -174,6 +174,8 @@ private ChainData buildChain(Matcher m) { /** Resolve the starting context for a chain */ private ChainContext resolveChainStart(ChainData chain, int chainStart) { ChainContext ctx = new ChainContext(); + ctx.chain = chain; + ctx.currentIndex = 0; String first = chain.segments.get(0); ctx.firstIsThis = first.equals("this"); @@ -181,15 +183,15 @@ private ChainContext resolveChainStart(ChainData chain, int chainStart) { ctx.firstIsPrecededByDot = document.isPrecededByDot(chainStart); ctx.startIndex = ctx.firstIsPrecededByDot ? 0 : 1; ctx.currentType = null; + ctx.enclosingType = document.findEnclosingScriptType(chainStart); if (ctx.firstIsThis) { - // 'this' - will use globalFields for resolution - ctx.currentType = null; + // 'this' - resolve to enclosing class/global fields + ctx.currentType = ctx.enclosingType; } else if (ctx.firstIsSuper) { // 'super' - resolve to parent class - ScriptTypeInfo enclosingType = document.findEnclosingScriptType(chainStart); - if (enclosingType != null && enclosingType.hasSuperClass()) { - ctx.currentType = enclosingType.getSuperClass(); + if (ctx.enclosingType != null && ctx.enclosingType.hasSuperClass()) { + ctx.currentType = ctx.enclosingType.getSuperClass(); } } else if (ctx.firstIsPrecededByDot) { // Field access on a receiver (e.g., getMinecraft().thePlayer) @@ -213,26 +215,29 @@ private ChainContext resolveChainStart(ChainData chain, int chainStart) { } /** Resolve what mark a segment should get */ - private MarkResult resolveSegmentMark(ChainContext ctx, String segment, int[] pos, - int index, boolean isLast, boolean isStatic, int chainStart) { + private MarkResult resolveSegmentMark(ChainContext ctx, int chainStart) { + int index = ctx.currentIndex; + String segment = ctx.chain.segments.get(index); + int[] pos = ctx.chain.positions.get(index); + // Case 1: First segment preceded by dot (field on receiver) if (index == 0 && ctx.firstIsPrecededByDot) { - return resolveReceiverFieldMark(segment, pos, chainStart, isLast, isStatic); + return resolveReceiverFieldMark(ctx, chainStart); } // Case 2: this.field if (index == 1 && ctx.firstIsThis) { - return resolveThisFieldMark(segment, pos); + return resolveThisFieldMark(ctx); } // Case 3: super.field if (index == 1 && ctx.firstIsSuper) { - return resolveSuperFieldMark(ctx.currentType, segment, pos, isLast); + return resolveSuperFieldMark(ctx); } // Case 4: Resolved type with field if (ctx.currentType != null && ctx.currentType.isResolved()) { - return resolveTypedFieldMark(ctx.currentType, segment, pos, isLast, isStatic); + return resolveTypedFieldMark(ctx); } // Case 5: Unresolved - mark as undefined @@ -245,20 +250,21 @@ private MarkResult resolveSegmentMark(ChainContext ctx, String segment, int[] po // ==================== SEGMENT RESOLUTION HELPERS ==================== /** Resolve mark for field access on a receiver (preceded by dot) */ - private MarkResult resolveReceiverFieldMark(String segment, int[] pos, int chainStart, - boolean isLast, boolean isStatic) { + private MarkResult resolveReceiverFieldMark(ChainContext ctx, int chainStart) { + int index = ctx.currentIndex; + String segment = ctx.chain.segments.get(index); + int[] pos = ctx.chain.positions.get(index); + boolean isLast = (index == ctx.chain.segments.size() - 1); + boolean isStatic = isStaticContext(ctx); + TypeInfo receiverType = document.resolveReceiverChain(chainStart); if (receiverType != null && receiverType.hasField(segment)) { FieldInfo fieldInfo = receiverType.getFieldInfo(segment); FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], receiverType, fieldInfo, isLast, isStatic); - - TokenType tokenType = (fieldInfo != null && fieldInfo.isEnumConstant()) - ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; - - return new MarkResult( - new ScriptLine.Mark(pos[0], pos[1], tokenType, accessInfo), + + return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo), (fieldInfo != null) ? fieldInfo.getTypeInfo() : null ); } @@ -270,23 +276,45 @@ private MarkResult resolveReceiverFieldMark(String segment, int[] pos, int chain } /** Resolve mark for this.field access */ - private MarkResult resolveThisFieldMark(String segment, int[] pos) { - if (document.getGlobalFields().containsKey(segment)) { - FieldInfo fieldInfo = document.getGlobalFields().get(segment); - return new MarkResult( - new ScriptLine.Mark(pos[0], pos[1], TokenType.GLOBAL_FIELD, fieldInfo), - fieldInfo.getTypeInfo() + private MarkResult resolveThisFieldMark(ChainContext ctx) { + int index = ctx.currentIndex; + String segment = ctx.chain.segments.get(index); + int[] pos = ctx.chain.positions.get(index); + boolean isLast = (index == ctx.chain.segments.size() - 1); + + boolean found = false; + FieldInfo fieldInfo = null; + + // First check enclosing script type fields + if (ctx.enclosingType != null && ctx.enclosingType.hasField(segment)) { + found = true; + fieldInfo = ctx.enclosingType.getFieldInfo(segment); + } else if (document.getGlobalFields().containsKey(segment)) { + found = true; + fieldInfo = document.getGlobalFields().get(segment); + } + + if (found) { + FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], ctx.enclosingType, + fieldInfo, isLast, false); + + return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo), + (fieldInfo != null) ? fieldInfo.getTypeInfo() : null ); } - - return new MarkResult( - new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), - null + + return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), null ); } /** Resolve mark for super.field access */ - private MarkResult resolveSuperFieldMark(TypeInfo superType, String segment, int[] pos, boolean isLast) { + private MarkResult resolveSuperFieldMark(ChainContext ctx) { + int index = ctx.currentIndex; + String segment = ctx.chain.segments.get(index); + int[] pos = ctx.chain.positions.get(index); + boolean isLast = (index == ctx.chain.segments.size() - 1); + TypeInfo superType = ctx.currentType; + if (superType == null) { return new MarkResult( new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR, @@ -311,8 +339,7 @@ private MarkResult resolveSuperFieldMark(TypeInfo superType, String segment, int if (found) { FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], superType, fieldInfo, isLast, false); - return new MarkResult( - new ScriptLine.Mark(pos[0], pos[1], TokenType.GLOBAL_FIELD, accessInfo), + return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo), (fieldInfo != null) ? fieldInfo.getTypeInfo() : null ); } @@ -327,8 +354,14 @@ private MarkResult resolveSuperFieldMark(TypeInfo superType, String segment, int } /** Resolve mark for typed field access */ - private MarkResult resolveTypedFieldMark(TypeInfo currentType, String segment, int[] pos, - boolean isLast, boolean isStatic) { + private MarkResult resolveTypedFieldMark(ChainContext ctx) { + int index = ctx.currentIndex; + String segment = ctx.chain.segments.get(index); + int[] pos = ctx.chain.positions.get(index); + boolean isLast = (index == ctx.chain.segments.size() - 1); + boolean isStatic = isStaticContext(ctx); + TypeInfo currentType = ctx.currentType; + if (!currentType.hasField(segment)) { return new MarkResult( new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), @@ -353,12 +386,8 @@ private MarkResult resolveTypedFieldMark(TypeInfo currentType, String segment, i // Valid field access FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], currentType, fieldInfo, isLast, isStatic); - - TokenType tokenType = (fieldInfo != null && fieldInfo.isEnumConstant()) - ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; - return new MarkResult( - new ScriptLine.Mark(pos[0], pos[1], tokenType, accessInfo), + return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo), (fieldInfo != null) ? fieldInfo.getTypeInfo() : null ); } @@ -373,14 +402,13 @@ private void markReceiverChainSegments(List marks, String first if (receiverType != null && receiverType.hasField(firstField)) { fInfo = receiverType.getFieldInfo(firstField); boolean hasMore = document.isFollowedByDot(identEnd); - boolean isStatic = isStaticContext(receiverType.getSimpleName()); + // For method/array result chains, the receiver is always an instance, not a type + boolean isStatic = false; FieldAccessInfo accessInfo = document.createFieldAccessInfo(firstField, identStart, identEnd, receiverType, fInfo, !hasMore, isStatic); - - TokenType tokenType = (fInfo != null && fInfo.isEnumConstant()) - ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; - marks.add(new ScriptLine.Mark(identStart, identEnd, tokenType, accessInfo)); + + marks.add(new ScriptLine.Mark(identStart, identEnd, getFieldTokenType(fInfo), accessInfo)); currentType = (fInfo != null) ? fInfo.getTypeInfo() : null; } else { marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR)); @@ -411,14 +439,13 @@ private void markReceiverChainSegments(List marks, String first if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) { FieldInfo segInfo = currentType.getFieldInfo(seg); - boolean isStatic = isStaticContext(currentType.getSimpleName()); + // In method/array result chains, segments are always instance access + boolean isStatic = false; FieldAccessInfo accessInfo = document.createFieldAccessInfo(seg, nStart, nEnd, currentType, segInfo, isLast, isStatic); - - TokenType tokenType = (segInfo != null && segInfo.isEnumConstant()) - ? TokenType.ENUM_CONSTANT : TokenType.GLOBAL_FIELD; - marks.add(new ScriptLine.Mark(nStart, nEnd, tokenType, accessInfo)); + + marks.add(new ScriptLine.Mark(nStart, nEnd, getFieldTokenType(segInfo), accessInfo)); currentType = (segInfo != null) ? segInfo.getTypeInfo() : null; } else { marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR)); @@ -429,6 +456,13 @@ private void markReceiverChainSegments(List marks, String first // ==================== UTILITY HELPERS ==================== + private TokenType getFieldTokenType(FieldInfo fieldInfo) { + if (fieldInfo != null && fieldInfo.isEnumConstant()) + return TokenType.ENUM_CONSTANT; + + return TokenType.GLOBAL_FIELD; + } + /** Get the non-whitespace character before a position */ private Character getNonWhitespaceBefore(int pos) { int before = pos - 1; @@ -436,8 +470,16 @@ private Character getNonWhitespaceBefore(int pos) { return (before >= 0) ? text.charAt(before) : null; } - /** Check if a name indicates static context (starts with uppercase) */ - private boolean isStaticContext(String name) { - return name != null && !name.isEmpty() && Character.isUpperCase(name.charAt(0)); + /** Check if the receiver type is a class/type (static context) */ + private boolean isStaticContext(ChainContext ctx) { + // Static context if the previous segment name resolves to a type/class + if (ctx.currentIndex <= 0) + return false; + + String previousSegment = ctx.chain.segments.get(ctx.currentIndex - 1); + + // Try to resolve the previous segment as a type + TypeInfo typeCheck = document.resolveType(previousSegment); + return typeCheck != null && typeCheck.isResolved(); } } From e3228632a33c7cca505ac63b4f40642adb441a6a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 21:55:18 +0200 Subject: [PATCH 145/337] oops forgor --- .../script/interpreter/ScriptDocument.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 95b3b099f..bf33d11af 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 @@ -295,6 +295,21 @@ public boolean isExcluded(int position) { return false; } + /** + * Check if a position is within a comment range (not string). + * Used to skip comment text when scanning for identifiers. + */ + private boolean isInCommentRange(int position) { + // Check if position is in a comment by checking against COMMENT_PATTERN + Matcher m = COMMENT_PATTERN.matcher(text); + while (m.find()) { + if (position >= m.start() && position < m.end()) { + return true; + } + } + return false; + } + // ==================== PHASE 2: IMPORTS ==================== private void parseImports() { @@ -4618,6 +4633,25 @@ boolean isInImportOrPackage(int position) { return false; } + /** Check if position is followed by '(' (method call) */ + boolean isFollowedByParen(int pos) { + int check = skipWhitespace(pos); + return check < text.length() && text.charAt(check) == '('; + } + + /** Check if position is followed by '.' */ + boolean isFollowedByDot(int pos) { + int check = skipWhitespace(pos); + return check < text.length() && text.charAt(check) == '.'; + } + + /** Skip whitespace and return new position */ + int skipWhitespace(int pos) { + while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) + pos++; + return pos; + } + private MethodInfo findMethodAtPosition(int position) { for (MethodInfo method : getAllMethods()) { if (method.containsPosition(position)) { From 5be3d934b78470f18457400573376c86ae4392a9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 22:08:53 +0200 Subject: [PATCH 146/337] Marked global variables for script types --- .../script/interpreter/ScriptDocument.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) 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 bf33d11af..5b57beffc 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 @@ -1857,7 +1857,7 @@ private void markTypeDeclarations(List marks) { int typeNameStart = m.start(1); int typeNameEnd = m.end(1); - if (isExcluded(typeNameStart)) { + if (isExcluded(typeNameStart) || isInImportOrPackage(typeNameStart)) { searchFrom = m.end(); continue; } @@ -3703,10 +3703,26 @@ private void markVariables(List marks) { // Check global fields if (globalFields.containsKey(name)) { FieldInfo fieldInfo = globalFields.get(name); - Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); - continue; + if (fieldInfo.getDeclarationOffset() == position) { + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); + continue; + } } + + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + if (scriptType.hasField(name)) { + FieldInfo fieldInfo = scriptType.getFieldInfo(name); + if (fieldInfo.getDeclarationOffset() == position) { + + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, + callInfo) : fieldInfo; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); + continue; + } + } + } + // Skip uppercase if not a known field - type handling will deal with it if (isUppercase) From 4f00870b9b29f0dc559c06b775e020342cad16bc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 22:37:36 +0200 Subject: [PATCH 147/337] Improved marking logic of ALL variables in all scopes --- .../script/interpreter/ScriptDocument.java | 77 ++++++++++++------- 1 file changed, 51 insertions(+), 26 deletions(-) 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 5b57beffc..f83c8d4e2 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 @@ -3680,55 +3680,80 @@ private void markVariables(List marks) { // Otherwise, let type handling (markImportedClassUsages) handle it boolean isUppercase = Character.isUpperCase(name.charAt(0)); - if (containingMethod != null) { - // Check parameters - if (containingMethod.hasParameter(name)) { - FieldInfo paramInfo = containingMethod.getParameter(name); - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER, paramInfo)); - continue; - } + // Scope resolution order: + // 1. Method parameters (if inside method) + // 2. Method local variables (if inside method) + // 3. Enclosing type fields (if inside method) + // 4. Global fields + // 5. Script type fields + + // Check parameters first (method scope) + if (containingMethod != null && containingMethod.hasParameter(name)) { + FieldInfo paramInfo = containingMethod.getParameter(name); + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER, paramInfo)); + continue; + } - // Check local variables + // Check local variables (method scope) + if (containingMethod != null) { Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); if (locals != null && locals.containsKey(name)) { FieldInfo localInfo = locals.get(name); - // Only highlight if the position is after the declaration if (localInfo.isVisibleAt(position)) { Object metadata = callInfo != null ? new FieldInfo.ArgInfo(localInfo, callInfo) : localInfo; marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, metadata)); continue; } } - } else { - // Check global fields - if (globalFields.containsKey(name)) { - FieldInfo fieldInfo = globalFields.get(name); - if (fieldInfo.getDeclarationOffset() == position) { + } + + // Check enclosing type fields (when inside method) + if (containingMethod != null) { + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasField(name)) { + FieldInfo fieldInfo = enclosingType.getFieldInfo(name); + if (fieldInfo.isVisibleAt(position)) { Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); continue; } } - - for (ScriptTypeInfo scriptType : scriptTypes.values()) { - if (scriptType.hasField(name)) { - FieldInfo fieldInfo = scriptType.getFieldInfo(name); - if (fieldInfo.getDeclarationOffset() == position) { + } - Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, - callInfo) : fieldInfo; + // Check other script type fields (only if position is within that type's boundaries) + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + if (scriptType.hasField(name)) { + // Only check if position is within this script type's class body + if (position >= scriptType.getBodyStart() && position <= scriptType.getBodyEnd()) { + FieldInfo fieldInfo = scriptType.getFieldInfo(name); + if(fieldInfo.isEnumConstant()) + continue; + + if (fieldInfo.isVisibleAt(position)) { + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); continue; } } } - - - // Skip uppercase if not a known field - type handling will deal with it - if (isUppercase) + } + + // Check global fields + if (globalFields.containsKey(name)) { + FieldInfo fieldInfo = globalFields.get(name); + if (fieldInfo.isVisibleAt(position)) { + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); continue; + } + } - // Unknown variable inside method - mark as undefined + // Skip uppercase if not a known field - type handling will deal with it + if (isUppercase) + continue; + + // Unknown variable - mark as undefined + if (containingMethod != null) { marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR, callInfo)); } } From 791cb3ddc121477f0517bf852cbca2d3c1fff973 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 22:38:03 +0200 Subject: [PATCH 148/337] Provided correct metadata for enum constant marks --- .../client/gui/util/script/interpreter/ScriptDocument.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 f83c8d4e2..aeb37f2d4 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 @@ -1749,9 +1749,8 @@ private void markClassDeclarations(List marks) { */ private void markEnumConstants(List marks) { for (ScriptTypeInfo scriptType : scriptTypes.values()) { - if (scriptType.getKind() != TypeInfo.Kind.ENUM) { + if (!scriptType.isEnum()) continue; - } // Mark each enum constant for (EnumConstantInfo constant : scriptType.getEnumConstants().values()) { @@ -1761,7 +1760,7 @@ private void markEnumConstants(List marks) { // Always use ENUM_CONSTANT token type (blue + bold + italic) // Errors are shown via underline, not by changing the token type - marks.add(new ScriptLine.Mark(start, end, TokenType.ENUM_CONSTANT, constant)); + marks.add(new ScriptLine.Mark(start, end, TokenType.ENUM_CONSTANT, fieldInfo)); } } } From b6f69dbe8c17cb183ae2a558878ad3729195dede Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 2 Jan 2026 22:46:16 +0200 Subject: [PATCH 149/337] Fixed constructors buggin (is already defined in scope for all single constructors) --- .../npcs/client/gui/util/script/interpreter/ScriptDocument.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aeb37f2d4..277877257 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 @@ -4745,7 +4745,7 @@ public List getAllMethods() { for (ScriptTypeInfo scriptType : scriptTypes.values()) { allMethods.addAll(scriptType.getAllMethodsFlat()); // Include constructors so their parameters are recognized - allMethods.addAll(scriptType.getConstructors()); + // allMethods.addAll(scriptType.getConstructors()); } return allMethods; From 1c3a11d43367256528883c4a531154ac274b3710 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 01:29:15 +0200 Subject: [PATCH 150/337] Fixed parameters not highlighting for constructors --- .../util/script/interpreter/ScriptDocument.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 277877257..018df2d28 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 @@ -3638,7 +3638,10 @@ private void markVariables(List marks) { )); // First pass: mark method parameters in their declaration positions - for (MethodInfo method : getAllMethods()) { + List allMethods = getAllMethods(); + allMethods.addAll(getAllConstructors()); + + for (MethodInfo method : allMethods) { for (FieldInfo param : method.getParameters()) { int pos = param.getDeclarationOffset(); String name = param.getName(); @@ -4750,6 +4753,15 @@ public List getAllMethods() { return allMethods; } + + public List getAllConstructors() { + List allConstructors = new ArrayList<>(); + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + allConstructors.addAll(scriptType.getConstructors()); + } + return allConstructors; + } + public List getMethodCalls() { List allCalls = new ArrayList<>(methodCalls); From bc6e9786dab536b2a766619fed344eab00be9d13 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 01:29:41 +0200 Subject: [PATCH 151/337] Fixed error hover info for enums being broken --- .../script/interpreter/hover/TokenHoverInfo.java | 12 +++++++++--- .../gui/util/script/interpreter/token/Token.java | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 9f9fae12d..2e01e47f8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -184,7 +184,7 @@ private void extractErrors(Token token) { } // Show method-level errors if this is the method name itself - MethodCallInfo callInfo = token.getMethodCallInfo(); + MethodCallInfo callInfo = token.isEnumConstant()? containingCall : token.getMethodCallInfo(); if (callInfo != null) { if (callInfo.hasArgCountError()) { errors.add(callInfo.getErrorMessage()); @@ -337,12 +337,18 @@ private MethodCallInfo findMethodCallContainingPosition(Token token) { ScriptDocument doc = line.getParent(); int tokenStart = token.getGlobalStart(); - + for (MethodCallInfo call : doc.getMethodCalls()) { + boolean isWithinName = tokenStart >= call.getMethodNameStart() && tokenStart <= call.getMethodNameEnd(); + + // If this is an enum constant, return methodCall on name itself + if (token.isEnumConstant() && isWithinName) + return call; + // Check if token is within the argument list if (tokenStart >= call.getOpenParenOffset() && tokenStart <= call.getCloseParenOffset()) { // Make sure it's not the method name itself - if (tokenStart >= call.getMethodNameStart() && tokenStart <= call.getMethodNameEnd()) { + if (isWithinName) { continue; } return call; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java index 194878c76..b7b81af9e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/Token.java @@ -206,6 +206,9 @@ public boolean isTypeReference() { type == TokenType.ENUM_DECL; } + public boolean isEnumConstant() { + return type == TokenType.ENUM_CONSTANT; + } public boolean isResolved() { if (typeInfo != null) return typeInfo.isResolved(); if (fieldInfo != null) return fieldInfo.isResolved(); From 931858c554a61558007e47fb5307a9414f131f50 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 01:46:14 +0200 Subject: [PATCH 152/337] Returned unresolved TypeInfo for types not found --- .../script/interpreter/ScriptDocument.java | 9 +++++++-- .../script/interpreter/method/MethodInfo.java | 4 ++++ .../script/interpreter/token/TokenType.java | 18 +++++++++++------- .../script/interpreter/type/TypeResolver.java | 4 ++-- 4 files changed, 24 insertions(+), 11 deletions(-) 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 018df2d28..f719aa4a7 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 @@ -1396,7 +1396,7 @@ TypeInfo resolveType(String typeName) { */ private TypeInfo resolveTypeAndTrackUsage(String typeName) { if (typeName == null || typeName.isEmpty()) - return null; + return TypeInfo.unresolved(typeName, typeName); // Strip generics for resolution int genericStart = typeName.indexOf('<'); @@ -2019,7 +2019,12 @@ private void markMethodDeclarations(List marks) { } // Return type - marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); + TokenType returnToken = TokenType.UNDEFINED_VAR; + if (methodInfo != null) + returnToken = methodInfo.getReturnType().getTokenType(); + + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), returnToken, + methodInfo != null ? methodInfo.getReturnType() : null)); // Method name with MethodInfo metadata marks.add(new ScriptLine.Mark(m.start(2), m.end(2), TokenType.METHOD_DECL, methodInfo)); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 21a4cdc71..f39875b26 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -451,6 +451,10 @@ public interface TypeResolver { public void validate(String methodBodyText, boolean hasBody, TypeResolver typeResolver) { if (!isDeclaration) return; + // Don't error if return type unresolved + if(returnType != null && !returnType.isResolved()) + return; + // Validate parameters validateParameters(); boolean interfaceMember = containingType != null && containingType.isInterface(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index c67174491..52f7b245e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -77,23 +77,27 @@ public boolean isItalic() { return italic; } - public static int getColor(TypeInfo typeInfo) { - if (typeInfo == null) - return TokenType.IMPORTED_CLASS.getHexColor(); + public static TokenType getByType(TypeInfo typeInfo) { + if (typeInfo == null || !typeInfo.isResolved()) + return TokenType.UNDEFINED_VAR; switch (typeInfo.getKind()) { case INTERFACE: - return TokenType.INTERFACE_DECL.getHexColor(); + return TokenType.INTERFACE_DECL; case ENUM: - return TokenType.ENUM_DECL.getHexColor(); + return TokenType.ENUM_DECL; case CLASS: - return TokenType.CLASS_DECL.getHexColor(); + return TokenType.CLASS_DECL; default: break; } // Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly - return typeInfo.getTokenType().getHexColor(); + return typeInfo.getTokenType(); + } + + public static int getColor(TypeInfo typeInfo) { + return getByType(typeInfo).getHexColor(); } /** 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 a494c54b2..b686594a1 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 @@ -125,7 +125,7 @@ public TypeInfo resolveSimpleName(String simpleName, Map imports, Set wildcardPackages) { if (simpleName == null || simpleName.isEmpty()) { - return null; + return TypeInfo.unresolved(simpleName,simpleName); } // 1. Check explicit imports @@ -166,7 +166,7 @@ public TypeInfo resolveSimpleName(String simpleName, } } - return null; + return TypeInfo.unresolved(simpleName,simpleName); } /** From 4146b156a29372ce721c5bef9b5e9daf1aedea1f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 01:50:08 +0200 Subject: [PATCH 153/337] Removed "final" and "extends Enum" from enum hover info --- .../util/script/interpreter/hover/TokenHoverInfo.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 2e01e47f8..7724664f8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -478,10 +478,11 @@ private void extractClassInfo(Token token) { Class clazz = typeInfo.getJavaClass(); if (clazz != null) { + boolean isEnum = clazz.isEnum(); // Icon if (clazz.isInterface()) { iconIndicator = "I"; - } else if (clazz.isEnum()) { + } else if (isEnum) { iconIndicator = "E"; } else { iconIndicator = "C"; @@ -493,12 +494,12 @@ private void extractClassInfo(Token token) { // Modifiers if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor()); if (Modifier.isAbstract(mods) && !clazz.isInterface()) addSegment("abstract ", TokenType.MODIFIER.getHexColor()); - if (Modifier.isFinal(mods)) addSegment("final ", TokenType.MODIFIER.getHexColor()); + if (Modifier.isFinal(mods) && !isEnum) addSegment("final ", TokenType.MODIFIER.getHexColor()); // Class type keyword if (clazz.isInterface()) { addSegment("interface ", TokenType.MODIFIER.getHexColor()); - } else if (clazz.isEnum()) { + } else if (isEnum) { addSegment("enum ", TokenType.MODIFIER.getHexColor()); } else { addSegment("class ", TokenType.MODIFIER.getHexColor()); @@ -512,7 +513,7 @@ private void extractClassInfo(Token token) { // Extends Class superclass = clazz.getSuperclass(); - if (superclass != null && superclass != Object.class) { + if (superclass != null && superclass != Object.class && !isEnum) { addSegment(" extends ", TokenType.MODIFIER.getHexColor()); addSegment(superclass.getSimpleName(), getColorForClass(superclass)); } From b613ec86fe871f722ceab2b3e01c26fed3ea3a41 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 02:00:23 +0200 Subject: [PATCH 154/337] drawn KEYWORD token color for primitive types in hover info --- .../script/interpreter/hover/TokenHoverInfo.java | 4 +--- .../util/script/interpreter/token/TokenType.java | 13 +------------ .../script/interpreter/type/ScriptTypeInfo.java | 12 ------------ .../gui/util/script/interpreter/type/TypeInfo.java | 7 +++++-- 4 files changed, 7 insertions(+), 29 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 7724664f8..af12aa82e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -1130,10 +1130,8 @@ private int getColorForClass(Class clazz) { * Works for both Java-backed TypeInfo and ScriptTypeInfo. */ private int getColorForTypeInfo(TypeInfo typeInfo) { - if (typeInfo == null) return TokenType.IMPORTED_CLASS.getHexColor(); - // Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly - return typeInfo.getTokenType().getHexColor(); + return TokenType.getColor(typeInfo); } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index 52f7b245e..8ee1d0f29 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -80,18 +80,7 @@ public boolean isItalic() { public static TokenType getByType(TypeInfo typeInfo) { if (typeInfo == null || !typeInfo.isResolved()) return TokenType.UNDEFINED_VAR; - - switch (typeInfo.getKind()) { - case INTERFACE: - return TokenType.INTERFACE_DECL; - case ENUM: - return TokenType.ENUM_DECL; - case CLASS: - return TokenType.CLASS_DECL; - default: - break; - } - + // Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly return typeInfo.getTokenType(); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index eaeaf015f..d79a360ba 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -653,18 +653,6 @@ public ScriptTypeInfo getInnerClass(String name) { return null; } - @Override - public TokenType getTokenType() { - switch (getKind()) { - case INTERFACE: - return TokenType.INTERFACE_DECL; - case ENUM: - return TokenType.ENUM_DECL; - case CLASS: - default: - return TokenType.CLASS_DECL; - } - } // Script types are always considered resolved since they're defined in the script @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 0d389c93f..c49a095bf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -134,14 +134,17 @@ public static TypeInfo fromPrimitive(String typeName) { public boolean isInnerClass() { return enclosingType != null; } public boolean isInterface() {return kind == Kind.INTERFACE;} public boolean isEnum() {return kind == Kind.ENUM;} + public boolean isPrimitive() {return javaClass != null && javaClass.isPrimitive();} /** * Get the appropriate TokenType for highlighting this type. */ public TokenType getTokenType() { - if (!resolved) { + if (!resolved) return TokenType.UNDEFINED_VAR; - } + if(isPrimitive()) + return TokenType.KEYWORD; + switch (kind) { case INTERFACE: return TokenType.INTERFACE_DECL; From df8833103e9241c32ca315eacac7e5529b7e03f0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 02:02:54 +0200 Subject: [PATCH 155/337] Fixed IndexOutOfBounds --- .../client/gui/util/script/interpreter/ScriptDocument.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 f719aa4a7..fcd9c02e5 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 @@ -2313,9 +2313,12 @@ private int findMatchingParen(int openPos) { public List parseMethodArguments(int start, int end, MethodInfo methodInfo) { List args = new ArrayList<>(); - if (start >= end) { + if (start >= end) return args; // No arguments - } + + if(end > text.length()) + end = text.length(); + int depth = 0; int argStart = start; From f3ba05068d5f8581715ff1fd37bdf4561d3a98cc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 02:05:43 +0200 Subject: [PATCH 156/337] Fixed IndexOutOfBounds --- .../client/gui/util/script/interpreter/ScriptDocument.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 fcd9c02e5..2b25c63c2 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 @@ -2316,8 +2316,7 @@ public List parseMethodArguments(int start, int end, Me if (start >= end) return args; // No arguments - if(end > text.length()) - end = text.length(); + end = Math.min(end, text.length()); int depth = 0; From aeda4f0f39eff4ab2443727d78a2d3533036f57e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 02:35:46 +0200 Subject: [PATCH 157/337] Fixed parseLocalFields breaking with generics list List --- .../client/gui/util/script/interpreter/ScriptDocument.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 2b25c63c2..a43c355d3 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 @@ -825,9 +825,8 @@ private void parseLocalVariables() { // Pattern for local variable declarations: Type varName = or Type varName; // Allows capital var names like "Minecraft Capital = new Minecraft();" // Use [ \t] instead of \s to prevent matching across newlines - Pattern localDecl = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)[ \\t]+([A-Za-z_][a-zA-Z0-9_]*)[ \\t]*(=|;|,)"); - Matcher m = localDecl.matcher(bodyText); + + Matcher m = FIELD_DECL_PATTERN.matcher(bodyText); while (m.find()) { String typeName = m.group(1); String varName = m.group(2); From cf4f4a48a0562f863851719d89e2c30ee2a08846 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 02:39:48 +0200 Subject: [PATCH 158/337] Marked all un-marked identifiers as UNDEFINED --- .../script/interpreter/ScriptDocument.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) 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 a43c355d3..583ecc12a 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 @@ -1493,6 +1493,9 @@ private List buildMarks() { // Mark unused imports (after all other marks are built) markUnusedImports(marks); + + // Final pass: Mark any remaining unmarked identifiers as undefined + markUndefinedIdentifiers(marks); return marks; } @@ -1511,6 +1514,61 @@ private void markUnusedImports(List marks) { } } } + + /** + * Final pass: Mark any remaining unmarked identifiers as UNDEFINED_VAR. + * This should be called last, after all other marking passes are complete. + * Only marks identifiers that haven't been marked by any other pass. + */ + private void markUndefinedIdentifiers(List marks) { + // Build a set of all marked positions for fast lookup + // Use a boolean array for O(1) lookup + boolean[] markedPositions = new boolean[text.length()]; + for (ScriptLine.Mark mark : marks) { + for (int i = mark.start; i < mark.end && i < markedPositions.length; i++) { + markedPositions[i] = true; + } + } + + // Keywords that should not be marked as undefined + Set knownKeywords = new HashSet<>(Arrays.asList( + "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", + "null", "true", "false", "if", "else", "switch", "case", "for", "while", "do", + "try", "catch", "finally", "return", "throw", "var", "let", "const", "function", + "continue", "break", "this", "new", "typeof", "instanceof", "class", "interface", + "extends", "implements", "import", "package", "public", "private", "protected", + "static", "final", "abstract", "synchronized", "native", "default", "enum", + "throws", "super", "assert", "volatile", "transient" + )); + + // Find all identifiers + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); + Matcher m = identifier.matcher(text); + + while (m.find()) { + int start = m.start(1); + int end = m.end(1); + String name = m.group(1); + + // Skip if already marked + if (start < markedPositions.length && markedPositions[start]) { + continue; + } + + // Skip if in excluded region (string/comment) + if (isExcluded(start)) { + continue; + } + + // Skip keywords + if (knownKeywords.contains(name)) { + continue; + } + + // Mark as undefined + marks.add(new ScriptLine.Mark(start, end, TokenType.UNDEFINED_VAR, null)); + } + } private void addPatternMarks(List marks, Pattern pattern, TokenType type) { addPatternMarks(marks, pattern, type, 0); From 694869004235328024e0fc20388fb2e52801615d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 02:43:23 +0200 Subject: [PATCH 159/337] Proper enum formatting for hover info --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index af12aa82e..1fde43f32 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -805,7 +805,7 @@ private void extractEnumConstantInfo(Token token) { } // Enum constant name - addSegment(fieldInfo.getName(), TokenType.ENUM_CONSTANT.getHexColor()); + addSegment(token.getStylePrefix() + fieldInfo.getName(), TokenType.ENUM_CONSTANT.getHexColor()); // Add constructor arguments if available addInitializationTokens(token, fieldInfo); From 79d1b74a0f490f76df233ac455792093ed2cf1a2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 05:06:41 +0200 Subject: [PATCH 160/337] JS PARSER MK.I --- src/api | 2 +- .../client/gui/script/GuiScriptInterface.java | 3 + .../client/gui/util/GuiScriptTextArea.java | 20 + .../script/interpreter/ScriptDocument.java | 78 +- .../interpreter/ScriptTextContainer.java | 31 +- .../interpreter/js_parser/JSFieldInfo.java | 51 ++ .../interpreter/js_parser/JSHoverInfo.java | 117 +++ .../interpreter/js_parser/JSMethodInfo.java | 93 +++ .../js_parser/JSScriptAnalyzer.java | 388 ++++++++++ .../interpreter/js_parser/JSTypeInfo.java | 166 +++++ .../interpreter/js_parser/JSTypeRegistry.java | 379 ++++++++++ .../js_parser/TypeScriptDefinitionParser.java | 355 +++++++++ .../customnpcs/api/forge-events-raw.d.ts | 697 ++++++++++++++++++ .../assets/customnpcs/api/hooks.d.ts | 259 +++++++ .../assets/customnpcs/api/index.d.ts | 303 ++++++++ .../api/kamkeel/npcdbc/api/IDBCAddon.d.ts | 162 ++++ .../api/kamkeel/npcdbc/api/IKiAttack.d.ts | 22 + .../api/kamkeel/npcdbc/api/aura/IAura.d.ts | 19 + .../kamkeel/npcdbc/api/event/IDBCEvent.d.ts | 49 ++ .../api/kamkeel/npcdbc/api/form/IForm.d.ts | 55 ++ .../kamkeel/npcdbc/api/outline/IOutline.d.ts | 18 + .../assets/customnpcs/api/minecraft-raw.d.ts | 278 +++++++ .../minecraft/block/properties/IProperty.d.ts | 18 + .../minecraft/block/state/IBlockState.d.ts | 16 + .../api/net/minecraft/util/Vec3i.d.ts | 32 + .../api/net/minecraft/util/math/BlockPos.d.ts | 82 +++ .../net/minecraft/util/math/IntHashMap.d.ts | 75 ++ .../api/noppes/npcs/api/AbstractNpcAPI.d.ts | 126 ++++ .../api/noppes/npcs/api/IBlock.d.ts | 42 ++ .../api/noppes/npcs/api/ICommand.d.ts | 20 + .../api/noppes/npcs/api/IContainer.d.ts | 20 + .../api/noppes/npcs/api/IDamageSource.d.ts | 16 + .../customnpcs/api/noppes/npcs/api/INbt.d.ts | 43 ++ .../api/noppes/npcs/api/IParticle.d.ts | 86 +++ .../noppes/npcs/api/IPixelmonPlayerData.d.ts | 12 + .../customnpcs/api/noppes/npcs/api/IPos.d.ts | 44 ++ .../api/noppes/npcs/api/IScreenSize.d.ts | 14 + .../api/noppes/npcs/api/ISkinOverlay.d.ts | 30 + .../api/noppes/npcs/api/ITileEntity.d.ts | 24 + .../api/noppes/npcs/api/ITimers.d.ts | 24 + .../api/noppes/npcs/api/IWorld.d.ts | 139 ++++ .../noppes/npcs/api/block/IBlockScripted.d.ts | 55 ++ .../api/noppes/npcs/api/block/ITextPlane.d.ts | 26 + .../api/noppes/npcs/api/entity/IAnimal.d.ts | 17 + .../noppes/npcs/api/entity/IAnimatable.d.ts | 11 + .../api/noppes/npcs/api/entity/IArrow.d.ts | 13 + .../noppes/npcs/api/entity/ICustomNpc.d.ts | 217 ++++++ .../noppes/npcs/api/entity/IDBCPlayer.d.ts | 71 ++ .../api/noppes/npcs/api/entity/IEntity.d.ts | 101 +++ .../noppes/npcs/api/entity/IEntityItem.d.ts | 22 + .../noppes/npcs/api/entity/IEntityLiving.d.ts | 33 + .../npcs/api/entity/IEntityLivingBase.d.ts | 65 ++ .../api/noppes/npcs/api/entity/IFishHook.d.ts | 12 + .../api/noppes/npcs/api/entity/IMonster.d.ts | 8 + .../api/noppes/npcs/api/entity/IPixelmon.d.ts | 33 + .../api/noppes/npcs/api/entity/IPlayer.d.ts | 106 +++ .../noppes/npcs/api/entity/IProjectile.d.ts | 21 + .../noppes/npcs/api/entity/IThrowable.d.ts | 12 + .../api/noppes/npcs/api/entity/IVillager.d.ts | 13 + .../noppes/npcs/api/entity/data/IMark.d.ts | 16 + .../npcs/api/entity/data/IModelData.d.ts | 30 + .../npcs/api/entity/data/IModelRotate.d.ts | 17 + .../api/entity/data/IModelRotatePart.d.ts | 16 + .../npcs/api/entity/data/IModelScale.d.ts | 11 + .../npcs/api/entity/data/IModelScalePart.d.ts | 14 + .../npcs/api/event/IAnimationEvent.d.ts | 27 + .../noppes/npcs/api/event/IBlockEvent.d.ts | 51 ++ .../npcs/api/event/ICustomGuiEvent.d.ts | 32 + .../npcs/api/event/ICustomNPCsEvent.d.ts | 29 + .../noppes/npcs/api/event/IDialogEvent.d.ts | 19 + .../noppes/npcs/api/event/IFactionEvent.d.ts | 17 + .../noppes/npcs/api/event/IForgeEvent.d.ts | 19 + .../api/noppes/npcs/api/event/IItemEvent.d.ts | 66 ++ .../npcs/api/event/ILinkedItemEvent.d.ts | 16 + .../api/noppes/npcs/api/event/INpcEvent.d.ts | 79 ++ .../noppes/npcs/api/event/IPartyEvent.d.ts | 34 + .../noppes/npcs/api/event/IPlayerEvent.d.ts | 191 +++++ .../npcs/api/event/IProjectileEvent.d.ts | 21 + .../noppes/npcs/api/event/IQuestEvent.d.ts | 23 + .../noppes/npcs/api/event/IRecipeEvent.d.ts | 33 + .../api/noppes/npcs/api/gui/IButton.d.ts | 25 + .../api/noppes/npcs/api/gui/ICustomGui.d.ts | 47 ++ .../npcs/api/gui/ICustomGuiComponent.d.ts | 27 + .../api/noppes/npcs/api/gui/IItemSlot.d.ts | 14 + .../api/noppes/npcs/api/gui/ILabel.d.ts | 19 + .../api/noppes/npcs/api/gui/ILine.d.ts | 20 + .../api/noppes/npcs/api/gui/IScroll.d.ts | 19 + .../api/noppes/npcs/api/gui/ITextField.d.ts | 15 + .../noppes/npcs/api/gui/ITexturedRect.d.ts | 20 + .../npcs/api/handler/IActionManager.d.ts | 74 ++ .../npcs/api/handler/IAnimationHandler.d.ts | 17 + .../npcs/api/handler/IAttributeHandler.d.ts | 13 + .../npcs/api/handler/ICloneHandler.d.ts | 19 + .../api/handler/ICustomEffectHandler.d.ts | 32 + .../npcs/api/handler/IDialogHandler.d.ts | 12 + .../npcs/api/handler/IFactionHandler.d.ts | 14 + .../npcs/api/handler/IMagicHandler.d.ts | 14 + .../api/handler/INaturalSpawnsHandler.d.ts | 16 + .../npcs/api/handler/IOverlayHandler.d.ts | 16 + .../npcs/api/handler/IPartyHandler.d.ts | 12 + .../npcs/api/handler/IPlayerBankData.d.ts | 8 + .../noppes/npcs/api/handler/IPlayerData.d.ts | 22 + .../npcs/api/handler/IPlayerDialogData.d.ts | 13 + .../npcs/api/handler/IPlayerFactionData.d.ts | 13 + .../api/handler/IPlayerItemGiverData.d.ts | 13 + .../npcs/api/handler/IPlayerMailData.d.ts | 18 + .../npcs/api/handler/IPlayerQuestData.d.ts | 21 + .../api/handler/IPlayerTransportData.d.ts | 16 + .../npcs/api/handler/IProfileHandler.d.ts | 16 + .../npcs/api/handler/IQuestHandler.d.ts | 12 + .../npcs/api/handler/IRecipeHandler.d.ts | 18 + .../noppes/npcs/api/handler/ITagHandler.d.ts | 14 + .../npcs/api/handler/ITransportHandler.d.ts | 14 + .../noppes/npcs/api/handler/data/IAction.d.ts | 80 ++ .../npcs/api/handler/data/IActionChain.d.ts | 16 + .../api/handler/data/IActionListener.d.ts | 8 + .../npcs/api/handler/data/IActionQueue.d.ts | 46 ++ .../npcs/api/handler/data/IAnimation.d.ts | 36 + .../npcs/api/handler/data/IAnimationData.d.ts | 19 + .../npcs/api/handler/data/IAnvilRecipe.d.ts | 16 + .../handler/data/IAttributeDefinition.d.ts | 14 + .../npcs/api/handler/data/IAvailability.d.ts | 23 + .../api/handler/data/ICustomAttribute.d.ts | 12 + .../npcs/api/handler/data/ICustomEffect.d.ts | 32 + .../noppes/npcs/api/handler/data/IDialog.d.ts | 77 ++ .../api/handler/data/IDialogCategory.d.ts | 14 + .../npcs/api/handler/data/IDialogImage.d.ts | 36 + .../npcs/api/handler/data/IDialogOption.d.ts | 13 + .../npcs/api/handler/data/IFaction.d.ts | 34 + .../noppes/npcs/api/handler/data/IFrame.d.ts | 23 + .../npcs/api/handler/data/IFramePart.d.ts | 24 + .../noppes/npcs/api/handler/data/ILine.d.ts | 17 + .../noppes/npcs/api/handler/data/ILines.d.ts | 18 + .../npcs/api/handler/data/ILinkedItem.d.ts | 36 + .../noppes/npcs/api/handler/data/IMagic.d.ts | 21 + .../npcs/api/handler/data/IMagicCycle.d.ts | 17 + .../npcs/api/handler/data/IMagicData.d.ts | 17 + .../npcs/api/handler/data/INaturalSpawn.d.ts | 31 + .../noppes/npcs/api/handler/data/IParty.d.ts | 29 + .../npcs/api/handler/data/IPartyOptions.d.ts | 28 + .../api/handler/data/IPlayerAttributes.d.ts | 15 + .../npcs/api/handler/data/IPlayerEffect.d.ts | 20 + .../npcs/api/handler/data/IPlayerMail.d.ts | 23 + .../npcs/api/handler/data/IProfile.d.ts | 14 + .../api/handler/data/IProfileOptions.d.ts | 16 + .../noppes/npcs/api/handler/data/IQuest.d.ts | 36 + .../npcs/api/handler/data/IQuestCategory.d.ts | 14 + .../npcs/api/handler/data/IQuestDialog.d.ts | 8 + .../api/handler/data/IQuestInterface.d.ts | 8 + .../npcs/api/handler/data/IQuestItem.d.ts | 16 + .../npcs/api/handler/data/IQuestKill.d.ts | 12 + .../npcs/api/handler/data/IQuestLocation.d.ts | 16 + .../api/handler/data/IQuestObjective.d.ts | 17 + .../noppes/npcs/api/handler/data/IRecipe.d.ts | 23 + .../noppes/npcs/api/handler/data/ISlot.d.ts | 21 + .../noppes/npcs/api/handler/data/ISound.d.ts | 25 + .../noppes/npcs/api/handler/data/ITag.d.ts | 19 + .../api/handler/data/ITransportCategory.d.ts | 16 + .../api/handler/data/ITransportLocation.d.ts | 23 + .../data/actions/IConditionalAction.d.ts | 18 + .../api/noppes/npcs/api/item/IItemArmor.d.ts | 12 + .../api/noppes/npcs/api/item/IItemBlock.d.ts | 11 + .../api/noppes/npcs/api/item/IItemBook.d.ts | 16 + .../api/noppes/npcs/api/item/IItemCustom.d.ts | 21 + .../npcs/api/item/IItemCustomizable.d.ts | 44 ++ .../api/noppes/npcs/api/item/IItemLinked.d.ts | 11 + .../api/noppes/npcs/api/item/IItemStack.d.ts | 63 ++ .../api/noppes/npcs/api/jobs/IJob.d.ts | 12 + .../api/noppes/npcs/api/jobs/IJobBard.d.ts | 22 + .../npcs/api/jobs/IJobConversation.d.ts | 8 + .../noppes/npcs/api/jobs/IJobFollower.d.ts | 14 + .../api/noppes/npcs/api/jobs/IJobGuard.d.ts | 16 + .../api/noppes/npcs/api/jobs/IJobHealer.d.ts | 15 + .../noppes/npcs/api/jobs/IJobItemGiver.d.ts | 23 + .../api/noppes/npcs/api/jobs/IJobSpawner.d.ts | 16 + .../npcs/api/overlay/ICustomOverlay.d.ts | 26 + .../api/overlay/ICustomOverlayComponent.d.ts | 25 + .../npcs/api/overlay/IOverlayLabel.d.ts | 19 + .../noppes/npcs/api/overlay/IOverlayLine.d.ts | 20 + .../api/overlay/IOverlayTexturedRect.d.ts | 20 + .../api/noppes/npcs/api/roles/IRole.d.ts | 12 + .../api/noppes/npcs/api/roles/IRoleBank.d.ts | 8 + .../noppes/npcs/api/roles/IRoleFollower.d.ts | 27 + .../noppes/npcs/api/roles/IRoleMailman.d.ts | 8 + .../noppes/npcs/api/roles/IRoleTrader.d.ts | 28 + .../npcs/api/roles/IRoleTransporter.d.ts | 16 + .../npcs/api/scoreboard/IScoreboard.d.ts | 29 + .../api/scoreboard/IScoreboardObjective.d.ts | 15 + .../npcs/api/scoreboard/IScoreboardTeam.d.ts | 26 + .../customnpcs/api/scripts/item_events.d.ts | 24 + .../customnpcs/api/scripts/npc_events.d.ts | 32 + .../customnpcs/api/scripts/player_events.d.ts | 92 +++ 192 files changed, 8462 insertions(+), 14 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeScriptDefinitionParser.java create mode 100644 src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/hooks.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/index.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/util/Vec3i.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/util/math/IntHashMap.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/AbstractNpcAPI.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IBlock.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ICommand.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IContainer.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IDamageSource.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/INbt.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IParticle.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPixelmonPlayerData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPos.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IScreenSize.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ISkinOverlay.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITileEntity.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITimers.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IWorld.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/IBlockScripted.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/ITextPlane.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimal.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityItem.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGuiComponent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IItemSlot.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ILabel.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ILine.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IScroll.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ITextField.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ITexturedRect.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IActionManager.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IAnimationHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IAttributeHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ICloneHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ICustomEffectHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IDialogHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IFactionHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IMagicHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/INaturalSpawnsHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IOverlayHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPartyHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerBankData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerDialogData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerFactionData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerItemGiverData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerMailData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerQuestData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerTransportData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IProfileHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IQuestHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IRecipeHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ITagHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ITransportHandler.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAction.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IActionChain.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IActionListener.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IActionQueue.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAnimation.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAnimationData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAnvilRecipe.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAttributeDefinition.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAvailability.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ICustomAttribute.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ICustomEffect.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialog.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialogCategory.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialogImage.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialogOption.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IFaction.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IFrame.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IFramePart.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ILine.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ILines.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ILinkedItem.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IMagic.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IMagicCycle.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IMagicData.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/INaturalSpawn.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IParty.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPartyOptions.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPlayerAttributes.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPlayerEffect.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPlayerMail.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IProfile.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IProfileOptions.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuest.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestCategory.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestDialog.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestInterface.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestItem.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestKill.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestLocation.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestObjective.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IRecipe.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ISlot.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ISound.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ITag.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ITransportCategory.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ITransportLocation.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/actions/IConditionalAction.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemArmor.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemBlock.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemBook.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemCustom.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemCustomizable.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemLinked.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemStack.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJob.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobBard.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobConversation.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobFollower.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobGuard.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobHealer.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobItemGiver.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobSpawner.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/ICustomOverlay.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/ICustomOverlayComponent.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/IOverlayLabel.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/IOverlayLine.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/IOverlayTexturedRect.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRole.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleBank.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleFollower.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleMailman.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleTrader.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleTransporter.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/scoreboard/IScoreboard.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/scoreboard/IScoreboardObjective.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/scoreboard/IScoreboardTeam.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/scripts/item_events.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts create mode 100644 src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts diff --git a/src/api b/src/api index 305a3778c..d003d6d87 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 +Subproject commit d003d6d870db13d56fce223050a06faf15050eae 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 1a00de918..6a7d201b3 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -195,6 +195,9 @@ private void initScriptEditorTab(int yoffset) { activeArea.init(editorX, editorY, editorWidth, editorHeight, container == null ? "" : container.script); } + + // Set the scripting language for proper syntax highlighting + activeArea.setLanguage(this.handler.getLanguage()); // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { 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 c45c0b817..55ab59f97 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2331,6 +2331,26 @@ public void enableCodeHighlighting() { this.enableCodeHighlighting = true; this.container.formatCodeText(); } + + /** + * Set the scripting language for syntax highlighting and type inference. + * @param language The language name (e.g., "ECMAScript", "Groovy") + */ + public void setLanguage(String language) { + if (this.container != null) { + this.container.setLanguage(language); + if (this.enableCodeHighlighting) { + this.container.formatCodeText(); + } + } + } + + /** + * Get the current scripting language. + */ + public String getLanguage() { + return this.container != null ? this.container.getLanguage() : "ECMAScript"; + } 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 583ecc12a..fdf540934 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 @@ -8,6 +8,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSScriptAnalyzer; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; @@ -114,6 +115,9 @@ public class ScriptDocument { // Type resolver private final TypeResolver typeResolver; + + // Script language: "ECMAScript", "Groovy", etc. + private String language = "ECMAScript"; // Layout properties public int lineHeight = 13; @@ -132,6 +136,33 @@ public ScriptDocument(String text, TypeResolver resolver) { this.typeResolver = resolver != null ? resolver : TypeResolver.getInstance(); setText(text); } + + public ScriptDocument(String text, String language) { + this.typeResolver = TypeResolver.getInstance(); + this.language = language != null ? language : "ECMAScript"; + setText(text); + } + + /** + * Set the scripting language. + */ + public void setLanguage(String language) { + this.language = language != null ? language : "ECMAScript"; + } + + /** + * Get the scripting language. + */ + public String getLanguage() { + return language; + } + + /** + * Check if this is a JavaScript/ECMAScript document. + */ + public boolean isJavaScript() { + return "ECMAScript".equalsIgnoreCase(language); + } // ==================== TEXT MANAGEMENT ==================== @@ -219,18 +250,15 @@ public void formatCodeText() { methodCalls.clear(); externalFieldAssignments.clear(); declarationErrors.clear(); - - // Phase 1: Find excluded regions (strings/comments) - findExcludedRanges(); - - // Phase 2: Parse imports - parseImports(); - - // Phase 3: Parse structure (script types, methods, fields, locals) - parseStructure(); - - // Phase 4: Build marks and assign to lines - List marks = buildMarks(); + + List marks; + + // Use different analysis paths for JavaScript vs Java + if (isJavaScript()) { + marks = formatJavaScript(); + } else { + marks = formatJava(); + } // Phase 5: Resolve conflicts and sort marks = resolveConflicts(marks); @@ -243,6 +271,32 @@ public void formatCodeText() { // Phase 7: Compute indent guides computeIndentGuides(marks); } + + /** + * Format JavaScript/ECMAScript code using the JS type inference system. + */ + private List formatJavaScript() { + // Use JSScriptAnalyzer for JS-specific analysis + JSScriptAnalyzer analyzer = new JSScriptAnalyzer(text); + return analyzer.analyze(); + } + + /** + * Format Java/Groovy code using the full Java analysis system. + */ + private List formatJava() { + // Phase 1: Find excluded regions (strings/comments) + findExcludedRanges(); + + // Phase 2: Parse imports + parseImports(); + + // Phase 3: Parse structure (script types, methods, fields, locals) + parseStructure(); + + // Phase 4: Build marks and assign to lines + return buildMarks(); + } // ==================== PHASE 1: EXCLUDED RANGES ==================== 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 c2a26596b..b905ca412 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 @@ -31,6 +31,9 @@ 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"; public ScriptTextContainer(String text) { super(text); @@ -38,6 +41,32 @@ public ScriptTextContainer(String text) { document = new ScriptDocument(text); } } + + public ScriptTextContainer(String text, String language) { + super(text); + this.language = language != null ? language : "ECMAScript"; + if (USE_NEW_INTERPRETER) { + document = new ScriptDocument(text, this.language); + } + } + + /** + * Set the scripting language. This affects syntax highlighting and type inference. + * @param language The language name (e.g., "ECMAScript", "Groovy") + */ + public void setLanguage(String language) { + this.language = language != null ? language : "ECMAScript"; + if (document != null) { + document.setLanguage(this.language); + } + } + + /** + * Get the current scripting language. + */ + public String getLanguage() { + return language; + } @Override public void init(int width, int height) { @@ -73,7 +102,7 @@ public void init(String text, int width, int height) { return; } - document = new ScriptDocument(this.text); + document = new ScriptDocument(this.text, this.language); init(width, height); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java new file mode 100644 index 000000000..4ff653071 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java @@ -0,0 +1,51 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +/** + * Represents a field/property in a TypeScript interface. + */ +public class JSFieldInfo { + + private final String name; + private final String type; // Raw type string + private final boolean readonly; + private String documentation; + + public JSFieldInfo(String name, String type, boolean readonly) { + this.name = name; + this.type = type; + this.readonly = readonly; + } + + public JSFieldInfo setDocumentation(String documentation) { + this.documentation = documentation; + return this; + } + + // Getters + public String getName() { return name; } + public String getType() { return type; } + public boolean isReadonly() { return readonly; } + public String getDocumentation() { return documentation; } + + /** + * Build hover info HTML for this field. + */ + public String buildHoverInfo() { + StringBuilder sb = new StringBuilder(); + if (readonly) { + sb.append("readonly "); + } + sb.append("").append(name).append(": ").append(type).append(""); + + if (documentation != null && !documentation.isEmpty()) { + sb.append("

").append(documentation); + } + + return sb.toString(); + } + + @Override + public String toString() { + return (readonly ? "readonly " : "") + name + ": " + type; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java new file mode 100644 index 000000000..edc2879ca --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java @@ -0,0 +1,117 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import java.util.List; + +/** + * Container classes for hover information in JS scripts. + */ +public class JSHoverInfo { + + /** + * Hover info for a variable. + */ + public static class VariableInfo { + public final String name; + public final String typeName; + public final JSTypeInfo resolvedType; + + public VariableInfo(String name, String typeName, JSTypeInfo resolvedType) { + this.name = name; + this.typeName = typeName; + this.resolvedType = resolvedType; + } + + public String buildHoverHtml() { + StringBuilder sb = new StringBuilder(); + sb.append("").append(name).append(": ").append(typeName).append(""); + + if (resolvedType != null && resolvedType.getDocumentation() != null) { + sb.append("

").append(resolvedType.getDocumentation()); + } + + return sb.toString(); + } + } + + /** + * Hover info for a method. + */ + public static class MethodInfo { + public final JSMethodInfo method; + public final JSTypeInfo containingType; + + public MethodInfo(JSMethodInfo method, JSTypeInfo containingType) { + this.method = method; + this.containingType = containingType; + } + + public String buildHoverHtml() { + StringBuilder sb = new StringBuilder(); + + // Show containing type + if (containingType != null) { + sb.append("").append(containingType.getFullName()).append("
"); + } + + // Show method signature + sb.append(method.buildHoverInfo()); + + return sb.toString(); + } + } + + /** + * Hover info for a field. + */ + public static class FieldInfo { + public final JSFieldInfo field; + public final JSTypeInfo containingType; + + public FieldInfo(JSFieldInfo field, JSTypeInfo containingType) { + this.field = field; + this.containingType = containingType; + } + + public String buildHoverHtml() { + StringBuilder sb = new StringBuilder(); + + // Show containing type + if (containingType != null) { + sb.append("").append(containingType.getFullName()).append("
"); + } + + // Show field info + sb.append(field.buildHoverInfo()); + + return sb.toString(); + } + } + + /** + * Hover info for a hook function. + */ + public static class FunctionInfo { + public final String name; + public final List signatures; + + public FunctionInfo(String name, List signatures) { + this.name = name; + this.signatures = signatures; + } + + public String buildHoverHtml() { + StringBuilder sb = new StringBuilder(); + sb.append("Hook: ").append(name).append(""); + + if (!signatures.isEmpty()) { + sb.append("

Overloads:"); + for (JSTypeRegistry.HookSignature sig : signatures) { + sb.append("
• function ").append(name).append("(") + .append(sig.paramName).append(": ").append(sig.paramType).append(")"); + } + } + + return sb.toString(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java new file mode 100644 index 000000000..2d5c03d1d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java @@ -0,0 +1,93 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import java.util.*; + +/** + * Represents a method signature parsed from .d.ts files. + */ +public class JSMethodInfo { + + private final String name; + private final String returnType; // Raw type string, e.g., "number", "IEntity", "void" + private final List parameters; + private String documentation; + + public JSMethodInfo(String name, String returnType, List parameters) { + this.name = name; + this.returnType = returnType; + this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>(); + } + + public JSMethodInfo setDocumentation(String documentation) { + this.documentation = documentation; + return this; + } + + // Getters + public String getName() { return name; } + public String getReturnType() { return returnType; } + public List getParameters() { return parameters; } + public String getDocumentation() { return documentation; } + public int getParameterCount() { return parameters.size(); } + + /** + * Get a formatted signature string for display. + */ + public String getSignature() { + StringBuilder sb = new StringBuilder(); + sb.append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) sb.append(", "); + JSParameterInfo param = parameters.get(i); + sb.append(param.getName()).append(": ").append(param.getType()); + } + sb.append("): ").append(returnType); + return sb.toString(); + } + + /** + * Build hover info HTML for this method. + */ + public String buildHoverInfo() { + StringBuilder sb = new StringBuilder(); + sb.append("").append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) sb.append(", "); + JSParameterInfo param = parameters.get(i); + sb.append(param.getName()).append(": ").append(param.getType()).append(""); + } + sb.append("): ").append(returnType).append(""); + + if (documentation != null && !documentation.isEmpty()) { + sb.append("

").append(documentation); + } + + return sb.toString(); + } + + @Override + public String toString() { + return getSignature(); + } + + /** + * Represents a parameter in a method signature. + */ + public static class JSParameterInfo { + private final String name; + private final String type; + + public JSParameterInfo(String name, String type) { + this.name = name; + this.type = type; + } + + public String getName() { return name; } + public String getType() { return type; } + + @Override + public String toString() { + return name + ": " + type; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java new file mode 100644 index 000000000..6da33a795 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java @@ -0,0 +1,388 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; + +import java.util.*; +import java.util.regex.*; + +/** + * Analyzes JavaScript scripts to produce syntax highlighting marks and type information. + * Uses the JSTypeRegistry to resolve types and validate member access. + */ +public class JSScriptAnalyzer { + + private final String text; + private final JSTypeRegistry registry; + + // Variable tracking: varName -> inferred type name + private final Map variableTypes = new HashMap<>(); + + // Function parameter types: funcName -> paramName -> typeName + private final Map> functionParams = new HashMap<>(); + + // Excluded ranges (strings, comments) + private final List excludedRanges = new ArrayList<>(); + + // Patterns + private static final Pattern STRING_PATTERN = Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'"); + private static final Pattern COMMENT_PATTERN = Pattern.compile("//.*?$|/\\*.*?\\*/", Pattern.MULTILINE | Pattern.DOTALL); + private static final Pattern FUNCTION_PATTERN = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)"); + private static final Pattern VAR_DECL_PATTERN = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;]+))?"); + private static final Pattern ASSIGNMENT_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^;]+)"); + private static final Pattern METHOD_CALL_PATTERN = Pattern.compile("(\\w+(?:\\.\\w+)*)\\s*\\("); + private static final Pattern MEMBER_ACCESS_PATTERN = Pattern.compile("(\\w+)(?:\\.(\\w+))+"); + private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); + + private static final Pattern JS_KEYWORD_PATTERN = Pattern.compile( + "\\b(function|var|let|const|if|else|for|while|do|switch|case|break|continue|return|" + + "try|catch|finally|throw|new|typeof|instanceof|in|of|this|null|undefined|true|false|" + + "class|extends|import|export|default|async|await|yield)\\b"); + + private static final Pattern NUMBER_PATTERN = Pattern.compile("\\b\\d+\\.?\\d*\\b"); + + public JSScriptAnalyzer(String text) { + this.text = text; + this.registry = JSTypeRegistry.getInstance(); + // Ensure registry is initialized + if (!registry.isInitialized()) { + registry.initializeFromResources(); + } + } + + /** + * Analyze the script and produce marks. + */ + public List analyze() { + List marks = new ArrayList<>(); + + // First pass: find excluded regions + findExcludedRanges(); + + // Mark comments and strings + addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT); + addPatternMarks(marks, STRING_PATTERN, TokenType.STRING); + + // Mark keywords + addPatternMarks(marks, JS_KEYWORD_PATTERN, TokenType.KEYWORD); + + // Mark numbers + addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL); + + // Parse functions and infer parameter types from hooks + parseFunctions(marks); + + // Parse variable declarations + parseVariables(marks); + + // Mark member accesses with type validation + markMemberAccesses(marks); + + // Mark method calls + markMethodCalls(marks); + + return marks; + } + + /** + * Find strings and comments to exclude from analysis. + */ + private void findExcludedRanges() { + Matcher m = STRING_PATTERN.matcher(text); + while (m.find()) { + excludedRanges.add(new int[]{m.start(), m.end()}); + } + + m = COMMENT_PATTERN.matcher(text); + while (m.find()) { + excludedRanges.add(new int[]{m.start(), m.end()}); + } + } + + private boolean isExcluded(int pos) { + for (int[] range : excludedRanges) { + if (pos >= range[0] && pos < range[1]) return true; + } + return false; + } + + /** + * Parse function declarations and infer parameter types from hook signatures. + */ + private void parseFunctions(List marks) { + Matcher m = FUNCTION_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) continue; + + String funcName = m.group(1); + String params = m.group(2); + + // Mark function name + int nameStart = m.start(1); + int nameEnd = m.end(1); + + // Check if this is a known hook + if (registry.isHook(funcName)) { + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, + new JSHoverInfo.FunctionInfo(funcName, registry.getHookSignatures(funcName)))); + + // Infer parameter types from hook + List sigs = registry.getHookSignatures(funcName); + if (!sigs.isEmpty() && !params.isEmpty()) { + // Parse parameter names from function + String[] paramNames = params.split(","); + if (paramNames.length > 0) { + String paramName = paramNames[0].trim(); + // Use first hook signature's type + String paramType = sigs.get(0).paramType; + + // Store in function params and variable types + Map funcParamMap = new HashMap<>(); + funcParamMap.put(paramName, paramType); + functionParams.put(funcName, funcParamMap); + variableTypes.put(paramName, paramType); + + // Mark the parameter + int paramStart = m.start(2) + params.indexOf(paramName); + int paramEnd = paramStart + paramName.length(); + JSTypeInfo typeInfo = registry.getType(paramType); + marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER, + new JSHoverInfo.VariableInfo(paramName, paramType, typeInfo))); + } + } + } else { + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName)); + } + } + } + + /** + * Parse variable declarations and infer types. + */ + private void parseVariables(List marks) { + Matcher m = VAR_DECL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) continue; + + String varName = m.group(1); + String initializer = m.group(2); + + // Infer type from initializer + String inferredType = null; + if (initializer != null) { + inferredType = inferTypeFromExpression(initializer.trim()); + } + + if (inferredType != null) { + variableTypes.put(varName, inferredType); + } + + // Mark variable declaration + int varStart = m.start(1); + int varEnd = m.end(1); + JSTypeInfo typeInfo = inferredType != null ? registry.getType(inferredType) : null; + marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, + new JSHoverInfo.VariableInfo(varName, inferredType != null ? inferredType : "any", typeInfo))); + } + } + + /** + * Infer type from an expression. + */ + private String inferTypeFromExpression(String expr) { + if (expr == null || expr.isEmpty()) return null; + + // String literal + if (expr.startsWith("\"") || expr.startsWith("'")) { + return "string"; + } + + // Number literal + if (expr.matches("\\d+\\.?\\d*")) { + return "number"; + } + + // Boolean literal + if (expr.equals("true") || expr.equals("false")) { + return "boolean"; + } + + // null/undefined + if (expr.equals("null")) return "null"; + if (expr.equals("undefined")) return "undefined"; + + // Array literal + if (expr.startsWith("[")) { + return "any[]"; + } + + // Object literal + if (expr.startsWith("{")) { + return "object"; + } + + // Method call: something.method() - infer from method return type + if (expr.contains(".") && expr.contains("(")) { + return inferTypeFromMethodCall(expr); + } + + // Variable reference + if (variableTypes.containsKey(expr)) { + return variableTypes.get(expr); + } + + return null; + } + + /** + * Infer type from a method call chain. + */ + private String inferTypeFromMethodCall(String expr) { + // Remove trailing parentheses and args for analysis + int parenIndex = expr.indexOf('('); + if (parenIndex > 0) { + expr = expr.substring(0, parenIndex); + } + + String[] parts = expr.split("\\."); + if (parts.length < 2) return null; + + // Start with the receiver type + String currentType = variableTypes.get(parts[0]); + if (currentType == null) return null; + + // Walk the chain + for (int i = 1; i < parts.length; i++) { + JSTypeInfo typeInfo = registry.getType(currentType); + if (typeInfo == null) return null; + + String member = parts[i]; + + // Check if it's a method + JSMethodInfo method = typeInfo.getMethod(member); + if (method != null) { + currentType = method.getReturnType(); + continue; + } + + // Check if it's a field + JSFieldInfo field = typeInfo.getField(member); + if (field != null) { + currentType = field.getType(); + continue; + } + + return null; // Unknown member + } + + return currentType; + } + + /** + * Mark member accesses (x.y.z) with type validation. + */ + private void markMemberAccesses(List marks) { + Matcher m = MEMBER_ACCESS_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) continue; + + String fullAccess = m.group(0); + String[] parts = fullAccess.split("\\."); + + if (parts.length < 2) continue; + + // Get receiver type + String receiverName = parts[0]; + String currentType = variableTypes.get(receiverName); + + int pos = m.start(); + + // Mark receiver + if (currentType != null) { + JSTypeInfo typeInfo = registry.getType(currentType); + marks.add(new ScriptLine.Mark(pos, pos + receiverName.length(), TokenType.LOCAL_FIELD, + new JSHoverInfo.VariableInfo(receiverName, currentType, typeInfo))); + } + + pos += receiverName.length() + 1; // +1 for the dot + + // Walk the chain and mark each member + for (int i = 1; i < parts.length; i++) { + String member = parts[i]; + int memberStart = pos; + int memberEnd = pos + member.length(); + + if (currentType != null) { + JSTypeInfo typeInfo = registry.getType(currentType); + if (typeInfo != null) { + // Check method first + JSMethodInfo method = typeInfo.getMethod(member); + if (method != null) { + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.METHOD_CALL, + new JSHoverInfo.MethodInfo(method, typeInfo))); + currentType = method.getReturnType(); + } else { + // Check field + JSFieldInfo field = typeInfo.getField(member); + if (field != null) { + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.GLOBAL_FIELD, + new JSHoverInfo.FieldInfo(field, typeInfo))); + currentType = field.getType(); + } else { + // Unknown member - mark as undefined + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.UNDEFINED_VAR, + "Unknown member '" + member + "' on type " + typeInfo.getFullName())); + currentType = null; + } + } + } else { + currentType = null; + } + } + + pos = memberEnd + 1; // +1 for the next dot + } + } + } + + /** + * Mark method calls. + */ + private void markMethodCalls(List marks) { + Matcher m = METHOD_CALL_PATTERN.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) continue; + + String callExpr = m.group(1); + if (!callExpr.contains(".")) { + // Simple function call + int nameStart = m.start(1); + int nameEnd = m.end(1); + + if (registry.isHook(callExpr)) { + // It's a known hook being called + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, + new JSHoverInfo.FunctionInfo(callExpr, registry.getHookSignatures(callExpr)))); + } + } + // Chained calls are handled in markMemberAccesses + } + } + + /** + * Add pattern-based marks. + */ + private void addPatternMarks(List marks, Pattern pattern, TokenType type) { + Matcher m = pattern.matcher(text); + while (m.find()) { + marks.add(new ScriptLine.Mark(m.start(), m.end(), type)); + } + } + + /** + * Get the inferred type of a variable at a position. + */ + public String getVariableType(String varName) { + return variableTypes.get(varName); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java new file mode 100644 index 000000000..7efdd8481 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -0,0 +1,166 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import java.util.*; + +/** + * Represents a TypeScript interface/type parsed from .d.ts files. + * This is the JS equivalent of TypeInfo for Java types. + */ +public class JSTypeInfo { + + private final String simpleName; // e.g., "InteractEvent" + private final String fullName; // e.g., "IPlayerEvent.InteractEvent" + private final String namespace; // e.g., "IPlayerEvent" (parent namespace, null for top-level) + + // Members + private final Map methods = new LinkedHashMap<>(); + private final Map fields = new LinkedHashMap<>(); + + // Inheritance + private String extendsType; // The type this interface extends + private JSTypeInfo resolvedParent; // Resolved parent type (after registry is built) + + // Inner types (for namespaces) + private final Map innerTypes = new LinkedHashMap<>(); + private JSTypeInfo parentType; // The containing type (for inner types) + + // Documentation + private String documentation; + + public JSTypeInfo(String simpleName, String namespace) { + this.simpleName = simpleName; + this.namespace = namespace; + this.fullName = namespace != null ? namespace + "." + simpleName : simpleName; + } + + // Builder methods + public JSTypeInfo setExtends(String extendsType) { + this.extendsType = extendsType; + return this; + } + + public JSTypeInfo setDocumentation(String documentation) { + this.documentation = documentation; + return this; + } + + public void addMethod(JSMethodInfo method) { + // Handle overloads - store with index if name already exists + String key = method.getName(); + if (methods.containsKey(key)) { + // Find next available key for overload + int index = 1; + while (methods.containsKey(key + "$" + index)) { + index++; + } + methods.put(key + "$" + index, method); + } else { + methods.put(key, method); + } + } + + public void addField(JSFieldInfo field) { + fields.put(field.getName(), field); + } + + public void addInnerType(JSTypeInfo inner) { + inner.parentType = this; + innerTypes.put(inner.getSimpleName(), inner); + } + + public void setResolvedParent(JSTypeInfo parent) { + this.resolvedParent = parent; + } + + // Getters + public String getSimpleName() { return simpleName; } + public String getFullName() { return fullName; } + public String getNamespace() { return namespace; } + public String getExtendsType() { return extendsType; } + public JSTypeInfo getResolvedParent() { return resolvedParent; } + public String getDocumentation() { return documentation; } + public JSTypeInfo getParentType() { return parentType; } + + public Map getMethods() { return methods; } + public Map getFields() { return fields; } + public Map getInnerTypes() { return innerTypes; } + + /** + * Get a method by name, including inherited methods. + */ + public JSMethodInfo getMethod(String name) { + JSMethodInfo method = methods.get(name); + if (method != null) return method; + + // Check parent + if (resolvedParent != null) { + return resolvedParent.getMethod(name); + } + return null; + } + + /** + * Get all methods with a given name (for overloads). + */ + public List getMethodOverloads(String name) { + List overloads = new ArrayList<>(); + + // Get from this type + if (methods.containsKey(name)) { + overloads.add(methods.get(name)); + } + // Get numbered overloads + int index = 1; + while (methods.containsKey(name + "$" + index)) { + overloads.add(methods.get(name + "$" + index)); + index++; + } + + // Get from parent + if (resolvedParent != null) { + overloads.addAll(resolvedParent.getMethodOverloads(name)); + } + + return overloads; + } + + /** + * Check if this type has a method (including inherited). + */ + public boolean hasMethod(String name) { + return getMethod(name) != null; + } + + /** + * Get a field by name, including inherited fields. + */ + public JSFieldInfo getField(String name) { + JSFieldInfo field = fields.get(name); + if (field != null) return field; + + // Check parent + if (resolvedParent != null) { + return resolvedParent.getField(name); + } + return null; + } + + /** + * Check if this type has a field (including inherited). + */ + public boolean hasField(String name) { + return getField(name) != null; + } + + /** + * Get an inner type by name. + */ + public JSTypeInfo getInnerType(String name) { + return innerTypes.get(name); + } + + @Override + public String toString() { + return "JSTypeInfo{" + fullName + "}"; + } +} 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 new file mode 100644 index 000000000..7234197d1 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeRegistry.java @@ -0,0 +1,379 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import net.minecraft.client.Minecraft; +import net.minecraft.util.ResourceLocation; + +import java.io.*; +import java.util.*; + +/** + * Central registry for all TypeScript types parsed from .d.ts files. + * Also manages hook function signatures and type aliases. + */ +public class JSTypeRegistry { + + private static JSTypeRegistry INSTANCE; + + // Resource location for the API definitions + private static final String API_RESOURCE_PATH = "customnpcs:api/"; + + // All registered types by full name (e.g., "IPlayerEvent.InteractEvent") + private final Map types = new LinkedHashMap<>(); + + // 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 + private final Map> hooks = new LinkedHashMap<>(); + + // Primitive types + private static final Set PRIMITIVES = new HashSet<>(Arrays.asList( + "number", "string", "boolean", "void", "any", "null", "undefined", "never", "object" + )); + + private boolean initialized = false; + private boolean initializationAttempted = false; + + public static JSTypeRegistry getInstance() { + if (INSTANCE == null) { + INSTANCE = new JSTypeRegistry(); + } + return INSTANCE; + } + + private JSTypeRegistry() {} + + /** + * Initialize the registry from the embedded resources. + * This loads .d.ts files from assets/customnpcs/api/ + */ + public void initializeFromResources() { + if (initialized || initializationAttempted) return; + initializationAttempted = true; + + try { + TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); + + // Load hooks.d.ts first (defines function parameter types) + loadResourceFile(parser, "hooks.d.ts"); + + // Load index.d.ts (type aliases) + loadResourceFile(parser, "index.d.ts"); + + // Load main API files + loadResourceDirectory(parser, "noppes/npcs/api/"); + loadResourceDirectory(parser, "noppes/npcs/api/entity/"); + loadResourceDirectory(parser, "noppes/npcs/api/event/"); + loadResourceDirectory(parser, "noppes/npcs/api/handler/"); + loadResourceDirectory(parser, "noppes/npcs/api/item/"); + loadResourceDirectory(parser, "noppes/npcs/api/block/"); + loadResourceDirectory(parser, "noppes/npcs/api/gui/"); + + resolveInheritance(); + initialized = true; + System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from resources"); + } catch (Exception e) { + System.err.println("[JSTypeRegistry] Failed to load type definitions from resources: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Load a specific .d.ts file from resources. + */ + private void loadResourceFile(TypeScriptDefinitionParser parser, String fileName) { + try { + ResourceLocation loc = new ResourceLocation("customnpcs", "api/" + fileName); + InputStream is = Minecraft.getMinecraft().getResourceManager().getResource(loc).getInputStream(); + if (is != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + parser.parseDefinitionFile(sb.toString(), fileName); + } + } + } catch (Exception e) { + // File might not exist, that's ok + System.out.println("[JSTypeRegistry] Could not load " + fileName + ": " + e.getMessage()); + } + } + + /** + * Load all .d.ts files from a resource directory. + */ + private void loadResourceDirectory(TypeScriptDefinitionParser parser, String dirPath) { + // In Minecraft resources, we can't list directory contents directly + // So we'll try to load known files from a manifest or just try common patterns + // For now, let's try loading specific known files + String[] knownFiles = getKnownFilesForDirectory(dirPath); + for (String file : knownFiles) { + loadResourceFile(parser, dirPath + file); + } + } + + /** + * Get list of known .d.ts files for a directory. + * This is a workaround since we can't list resource directories. + */ + private String[] getKnownFilesForDirectory(String dirPath) { + // These are the known API files based on the actual file structure + if (dirPath.endsWith("noppes/npcs/api/")) { + return new String[]{ + "AbstractNpcAPI.d.ts", "IBlock.d.ts", "ICommand.d.ts", "IContainer.d.ts", + "IDamageSource.d.ts", "INbt.d.ts", "IParticle.d.ts", "IPixelmonPlayerData.d.ts", + "IPos.d.ts", "IScreenSize.d.ts", "ISkinOverlay.d.ts", "ITileEntity.d.ts", + "ITimers.d.ts", "IWorld.d.ts" + }; + } else if (dirPath.endsWith("entity/")) { + return new String[]{ + "ICustomNpc.d.ts", "IEntity.d.ts", "IEntityItem.d.ts", "IEntityLiving.d.ts", + "IEntityLivingBase.d.ts", "IPlayer.d.ts", "IProjectile.d.ts", "IAnimal.d.ts", + "IAnimatable.d.ts", "IArrow.d.ts", "IDBCPlayer.d.ts", "IFishHook.d.ts", + "IMonster.d.ts", "IPixelmon.d.ts", "IThrowable.d.ts", "IVillager.d.ts" + }; + } else if (dirPath.endsWith("event/")) { + return new String[]{ + "INpcEvent.d.ts", "IPlayerEvent.d.ts", "IItemEvent.d.ts", "IBlockEvent.d.ts", + "IDialogEvent.d.ts", "IQuestEvent.d.ts", "ICustomGuiEvent.d.ts", + "IAnimationEvent.d.ts", "ICustomNPCsEvent.d.ts", "IFactionEvent.d.ts", + "IForgeEvent.d.ts", "ILinkedItemEvent.d.ts", "IPartyEvent.d.ts", + "IProjectileEvent.d.ts", "IRecipeEvent.d.ts" + }; + } else if (dirPath.endsWith("handler/")) { + return new String[]{ + "ICloneHandler.d.ts", "IDialogHandler.d.ts", "IFactionHandler.d.ts", + "IQuestHandler.d.ts", "IRecipeHandler.d.ts", "ITagHandler.d.ts", + "IAnimationHandler.d.ts", "IActionManager.d.ts", "IAttributeHandler.d.ts", + "ICustomEffectHandler.d.ts", "IMagicHandler.d.ts", "INaturalSpawnsHandler.d.ts", + "IOverlayHandler.d.ts", "IPartyHandler.d.ts", "IPlayerBankData.d.ts", + "IPlayerData.d.ts", "IPlayerDialogData.d.ts", "IPlayerFactionData.d.ts", + "IPlayerItemGiverData.d.ts", "IPlayerMailData.d.ts", "IPlayerQuestData.d.ts", + "IPlayerTransportData.d.ts", "IProfileHandler.d.ts", "ITransportHandler.d.ts" + }; + } else if (dirPath.endsWith("item/")) { + return new String[]{ + "IItemStack.d.ts", "IItemArmor.d.ts", "IItemBook.d.ts", "IItemBlock.d.ts", + "IItemCustom.d.ts", "IItemCustomizable.d.ts", "IItemLinked.d.ts" + }; + } else if (dirPath.endsWith("block/")) { + return new String[]{ + "IBlockScripted.d.ts", "ITextPlane.d.ts" + }; + } else if (dirPath.endsWith("gui/")) { + return new String[]{ + "IButton.d.ts", "ICustomGui.d.ts", "ILabel.d.ts", "ITextField.d.ts", + "ITexturedRect.d.ts", "IScroll.d.ts", "IItemSlot.d.ts", "ICustomGuiComponent.d.ts", + "ILine.d.ts" + }; + } + return new String[]{}; + } + + /** + * Initialize the registry from a directory containing .d.ts files. + */ + public void initializeFromDirectory(File directory) { + if (initialized) return; + + try { + TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); + parser.parseDirectory(directory); + resolveInheritance(); + initialized = true; + System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks"); + } catch (IOException e) { + System.err.println("[JSTypeRegistry] Failed to load type definitions: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Initialize the registry from a VSIX file. + */ + public void initializeFromVsix(File vsixFile) { + if (initialized) return; + + try { + TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); + parser.parseVsixArchive(vsixFile); + resolveInheritance(); + initialized = true; + System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from VSIX"); + } catch (IOException e) { + System.err.println("[JSTypeRegistry] Failed to load VSIX: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Register a type. + */ + public void registerType(JSTypeInfo type) { + types.put(type.getFullName(), type); + } + + /** + * Register a type alias. + */ + public void registerTypeAlias(String alias, String fullType) { + typeAliases.put(alias, fullType); + } + + /** + * Register a hook function signature. + */ + public void registerHook(String functionName, String paramName, String paramType) { + hooks.computeIfAbsent(functionName, k -> new ArrayList<>()) + .add(new HookSignature(paramName, paramType)); + } + + /** + * Get a type by name (handles aliases and primitives). + */ + public JSTypeInfo getType(String name) { + return getType(name, new HashSet<>()); + } + + /** + * Internal method with cycle detection for type aliases. + */ + private JSTypeInfo getType(String name, Set visited) { + if (name == null || name.isEmpty()) return null; + + // Strip array brackets for lookup + String baseName = name.replace("[]", "").trim(); + + // Check if primitive + if (PRIMITIVES.contains(baseName)) { + return null; // Primitives don't have JSTypeInfo + } + + // Detect circular alias references + if (visited.contains(baseName)) { + System.err.println("[JSTypeRegistry] Circular type alias detected: " + baseName); + return null; + } + visited.add(baseName); + + // Direct lookup + if (types.containsKey(baseName)) { + return types.get(baseName); + } + + // Check type aliases + if (typeAliases.containsKey(baseName)) { + String resolved = typeAliases.get(baseName); + return getType(resolved, visited); + } + + // Try simple name lookup (for types like "IEntity" without namespace) + for (JSTypeInfo type : types.values()) { + if (type.getSimpleName().equals(baseName)) { + return type; + } + } + + return null; + } + + /** + * Check if a type name is a known primitive. + */ + public boolean isPrimitive(String typeName) { + return PRIMITIVES.contains(typeName); + } + + /** + * Get hook signatures for a function name. + */ + public List getHookSignatures(String functionName) { + return hooks.getOrDefault(functionName, Collections.emptyList()); + } + + /** + * Check if a function name is a known hook. + */ + public boolean isHook(String functionName) { + return hooks.containsKey(functionName); + } + + /** + * Get the parameter type for a hook function. + * If multiple overloads exist, returns the first one. + */ + public String getHookParameterType(String functionName) { + List sigs = hooks.get(functionName); + if (sigs != null && !sigs.isEmpty()) { + return sigs.get(0).paramType; + } + return null; + } + + /** + * Resolve inheritance relationships between types. + */ + private void resolveInheritance() { + for (JSTypeInfo type : types.values()) { + String extendsType = type.getExtendsType(); + if (extendsType != null) { + JSTypeInfo parent = getType(extendsType); + if (parent != null) { + type.setResolvedParent(parent); + } + } + } + } + + /** + * Get all registered types. + */ + public Collection getAllTypes() { + return types.values(); + } + + /** + * Get all registered hooks. + */ + public Map> getAllHooks() { + return hooks; + } + + /** + * Check if initialized. + */ + public boolean isInitialized() { + return initialized; + } + + /** + * Clear the registry (for reloading). + */ + public void clear() { + types.clear(); + typeAliases.clear(); + hooks.clear(); + initialized = false; + } + + /** + * Represents a hook function signature. + */ + public static class HookSignature { + public final String paramName; + public final String paramType; + + public HookSignature(String paramName, String paramType) { + this.paramName = paramName; + this.paramType = paramType; + } + + @Override + public String toString() { + return paramName + ": " + paramType; + } + } +} 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 new file mode 100644 index 000000000..726672b47 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeScriptDefinitionParser.java @@ -0,0 +1,355 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import java.io.*; +import java.util.*; +import java.util.regex.*; +import java.util.zip.*; + +/** + * Parses TypeScript definition files (.d.ts) to extract type information. + * Can read from individual files, directories, or .vsix archives. + */ +public class TypeScriptDefinitionParser { + + // Patterns for parsing .d.ts content + private static final Pattern INTERFACE_PATTERN = Pattern.compile( + "export\\s+interface\\s+(\\w+)(?:\\s+extends\\s+([\\w.]+))?\\s*\\{"); + + private static final Pattern NAMESPACE_PATTERN = Pattern.compile( + "export\\s+namespace\\s+(\\w+)\\s*\\{"); + + private static final Pattern TYPE_ALIAS_PATTERN = Pattern.compile( + "export\\s+type\\s+(\\w+)\\s*=\\s*([^;]+);"); + + private static final Pattern METHOD_PATTERN = Pattern.compile( + "^\\s*(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*([^;]+);", Pattern.MULTILINE); + + private static final Pattern FIELD_PATTERN = Pattern.compile( + "^\\s*(readonly\\s+)?(\\w+)\\s*:\\s*([^;]+);", Pattern.MULTILINE); + + 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.]+);"); + + private final JSTypeRegistry registry; + + public TypeScriptDefinitionParser(JSTypeRegistry registry) { + this.registry = registry; + } + + /** + * Parse all .d.ts files from a VSIX archive. + */ + public void parseVsixArchive(File vsixFile) throws IOException { + try (ZipFile zip = new ZipFile(vsixFile)) { + Enumeration entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".d.ts")) { + try (InputStream is = zip.getInputStream(entry); + BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + String content = readFully(reader); + String fileName = entry.getName(); + parseDefinitionFile(content, fileName); + } + } + } + } + } + + /** + * Parse all .d.ts files from a directory recursively. + */ + public void parseDirectory(File directory) throws IOException { + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + parseDirectoryRecursive(directory); + } + + private void parseDirectoryRecursive(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) return; + + for (File file : files) { + if (file.isDirectory()) { + parseDirectoryRecursive(file); + } else if (file.getName().endsWith(".d.ts")) { + parseFile(file); + } + } + } + + /** + * Parse a single .d.ts file. + */ + public void parseFile(File file) throws IOException { + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String content = readFully(reader); + parseDefinitionFile(content, file.getName()); + } + } + + /** + * Parse a .d.ts file content. + */ + public void parseDefinitionFile(String content, String fileName) { + // Special handling for hooks.d.ts - extract function signatures + if (fileName.contains("hooks.d.ts")) { + parseHooksFile(content); + return; + } + + // Special handling for index.d.ts - extract global type aliases + if (fileName.contains("index.d.ts")) { + parseIndexFile(content); + return; + } + + // Parse regular interface files + parseInterfaceFile(content, null); + } + + /** + * Parse hooks.d.ts to extract function signatures for JS hooks. + */ + 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 index.d.ts to extract global type aliases. + */ + private void parseIndexFile(String content) { + Matcher m = GLOBAL_TYPE_ALIAS_PATTERN.matcher(content); + while (m.find()) { + String aliasName = m.group(1); + String importPath = m.group(2); + String typeName = m.group(3); + registry.registerTypeAlias(aliasName, typeName); + } + } + + /** + * Parse interface definitions from content. + */ + private void parseInterfaceFile(String content, String parentNamespace) { + // Find interfaces + Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(content); + while (interfaceMatcher.find()) { + String interfaceName = interfaceMatcher.group(1); + String extendsType = interfaceMatcher.group(2); + + JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace); + if (extendsType != null) { + typeInfo.setExtends(extendsType); + } + + // Find the body of this interface + int bodyStart = interfaceMatcher.end(); + int bodyEnd = findMatchingBrace(content, bodyStart - 1); + if (bodyEnd > bodyStart) { + String body = content.substring(bodyStart, bodyEnd); + parseInterfaceBody(body, typeInfo); + } + + registry.registerType(typeInfo); + } + + // Find namespaces (which contain inner types) + Matcher namespaceMatcher = NAMESPACE_PATTERN.matcher(content); + while (namespaceMatcher.find()) { + String namespaceName = namespaceMatcher.group(1); + + // Find the body of this namespace + int bodyStart = namespaceMatcher.end(); + int bodyEnd = findMatchingBrace(content, bodyStart - 1); + if (bodyEnd > bodyStart) { + String body = content.substring(bodyStart, bodyEnd); + + // Recursively parse inner types with namespace prefix + String fullNamespace = parentNamespace != null ? + parentNamespace + "." + namespaceName : namespaceName; + parseInterfaceFile(body, fullNamespace); + + // Also handle type aliases within namespace + parseTypeAliases(body, fullNamespace); + } + } + + // Handle top-level type aliases + if (parentNamespace == null) { + parseTypeAliases(content, null); + } + } + + /** + * Parse type aliases (export type X = Y). + */ + private void parseTypeAliases(String content, String namespace) { + Matcher m = TYPE_ALIAS_PATTERN.matcher(content); + while (m.find()) { + String aliasName = m.group(1); + String targetType = m.group(2).trim(); + + // Create a simple type that extends the target + JSTypeInfo typeInfo = new JSTypeInfo(aliasName, namespace); + typeInfo.setExtends(targetType); + registry.registerType(typeInfo); + } + } + + /** + * Parse the body of an interface to extract methods and fields. + */ + private void parseInterfaceBody(String body, JSTypeInfo typeInfo) { + // Parse methods + Matcher methodMatcher = METHOD_PATTERN.matcher(body); + while (methodMatcher.find()) { + String methodName = methodMatcher.group(1); + String params = methodMatcher.group(2); + String returnType = cleanType(methodMatcher.group(3)); + + List parameters = parseParameters(params); + JSMethodInfo method = new JSMethodInfo(methodName, returnType, parameters); + typeInfo.addMethod(method); + } + + // Parse fields (that aren't method signatures) + Matcher fieldMatcher = FIELD_PATTERN.matcher(body); + while (fieldMatcher.find()) { + String fieldText = fieldMatcher.group(0); + // Skip if this looks like a method (has parentheses in the match) + if (fieldText.contains("(")) continue; + + boolean readonly = fieldMatcher.group(1) != null; + String fieldName = fieldMatcher.group(2); + String fieldType = cleanType(fieldMatcher.group(3)); + + JSFieldInfo field = new JSFieldInfo(fieldName, fieldType, readonly); + typeInfo.addField(field); + } + } + + /** + * Parse parameters string into list of parameter info. + */ + private List parseParameters(String params) { + List result = new ArrayList<>(); + if (params == null || params.trim().isEmpty()) { + return result; + } + + // Split by comma, but be careful of nested types like Map + List paramParts = splitParameters(params); + + for (String part : paramParts) { + part = part.trim(); + if (part.isEmpty()) continue; + + // Format: name: type or name?: type + int colonIndex = part.indexOf(':'); + if (colonIndex > 0) { + String name = part.substring(0, colonIndex).trim().replace("?", ""); + String type = cleanType(part.substring(colonIndex + 1).trim()); + result.add(new JSMethodInfo.JSParameterInfo(name, type)); + } + } + + return result; + } + + /** + * Split parameters respecting nested angle brackets. + */ + private List splitParameters(String params) { + List result = new ArrayList<>(); + int depth = 0; + int start = 0; + + for (int i = 0; i < params.length(); i++) { + char c = params.charAt(i); + if (c == '<' || c == '(' || c == '[') { + depth++; + } else if (c == '>' || c == ')' || c == ']') { + depth--; + } else if (c == ',' && depth == 0) { + result.add(params.substring(start, i)); + start = i + 1; + } + } + + if (start < params.length()) { + result.add(params.substring(start)); + } + + return result; + } + + /** + * Clean up a type string (remove import() syntax, trim, etc.). + */ + private String cleanType(String type) { + if (type == null) return "any"; + type = type.trim(); + + // Handle import('./path').TypeName syntax + if (type.startsWith("import(")) { + // Extract the type name after the import + int dotIndex = type.lastIndexOf(")."); + if (dotIndex > 0) { + type = type.substring(dotIndex + 2); + } + } + + // Handle arrays + type = type.replace("[]", "[]"); + + return type; + } + + /** + * Find the matching closing brace. + */ + private int findMatchingBrace(String text, int openBracePos) { + int depth = 1; + for (int i = openBracePos + 1; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '{') depth++; + else if (c == '}') { + depth--; + if (depth == 0) return i; + } + } + return -1; + } + + /** + * Read entire content from reader. + */ + private String readFully(BufferedReader reader) throws IOException { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } +} diff --git a/src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts b/src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts new file mode 100644 index 000000000..30846c672 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts @@ -0,0 +1,697 @@ +/** + * Specialized Raw Forge 1.7.10 Event Definitions + * Deep package hierarchy to match Java source (Forge/MCP). + */ + +declare global { + namespace cpw.mods.fml.common.eventhandler { + /** cpw.mods.fml.common.eventhandler.Event */ + export interface Event { + /** @returns If this event can be canceled */ + isCancelable(): boolean; + /** @returns If this event has been canceled */ + isCanceled(): boolean; + /** Sets the canceled state */ + setCanceled(cancel: boolean): void; + /** @returns The current result (ALLOW, DENY, DEFAULT) */ + getResult(): 'ALLOW' | 'DENY' | 'DEFAULT'; + /** Sets the result */ + setResult(result: 'ALLOW' | 'DENY' | 'DEFAULT'): void; + /** @returns If this event has a result */ + hasResult(): boolean; + } + } + + namespace cpw.mods.fml.common.gameevent { + /** cpw.mods.fml.common.gameevent.TickEvent */ + export interface TickEvent extends cpw.mods.fml.common.eventhandler.Event { + type: 'PLAYER' | 'WORLD' | 'SERVER' | 'CLIENT' | 'RENDER'; + side: 'CLIENT' | 'SERVER'; + phase: 'START' | 'END'; + } + + export namespace TickEvent { + /** cpw.mods.fml.common.gameevent.TickEvent.PlayerTickEvent */ + export interface PlayerTickEvent extends cpw.mods.fml.common.gameevent.TickEvent { + player: net.minecraft.entity.player.EntityPlayer; + } + /** cpw.mods.fml.common.gameevent.TickEvent.WorldTickEvent */ + export interface WorldTickEvent extends cpw.mods.fml.common.gameevent.TickEvent { + world: net.minecraft.world.World; + } + /** cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent */ + export type ServerTickEvent = cpw.mods.fml.common.gameevent.TickEvent + /** cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent */ + export type ClientTickEvent = cpw.mods.fml.common.gameevent.TickEvent + /** cpw.mods.fml.common.gameevent.TickEvent.RenderTickEvent */ + export interface RenderTickEvent extends cpw.mods.fml.common.gameevent.TickEvent { + renderTickTime: number; + } + } + + /** cpw.mods.fml.common.gameevent.InputEvent */ + export type InputEvent = cpw.mods.fml.common.eventhandler.Event + export namespace InputEvent { + /** cpw.mods.fml.common.gameevent.InputEvent.KeyInputEvent */ + export type KeyInputEvent = InputEvent + /** cpw.mods.fml.common.gameevent.InputEvent.MouseInputEvent */ + export type MouseInputEvent = InputEvent + } + + /** cpw.mods.fml.common.gameevent.PlayerEvent */ + export interface PlayerEvent extends cpw.mods.fml.common.eventhandler.Event { + player: net.minecraft.entity.player.EntityPlayer; + } + export namespace PlayerEvent { + /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent */ + export type PlayerLoggedInEvent = cpw.mods.fml.common.gameevent.PlayerEvent + /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent */ + export type PlayerLoggedOutEvent = cpw.mods.fml.common.gameevent.PlayerEvent + /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerRespawnEvent */ + export type PlayerRespawnEvent = cpw.mods.fml.common.gameevent.PlayerEvent + /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerChangedDimensionEvent */ + export interface PlayerChangedDimensionEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { + fromDim: number; + toDim: number; + } + /** cpw.mods.fml.common.gameevent.PlayerEvent.ItemPickupEvent */ + export interface ItemPickupEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { + pickedUp: net.minecraft.entity.EntityItem; + } + /** cpw.mods.fml.common.gameevent.PlayerEvent.ItemCraftedEvent */ + export interface ItemCraftedEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { + craftMatrix: any; + crafting: net.minecraft.item.ItemStack; + } + /** cpw.mods.fml.common.gameevent.PlayerEvent.ItemSmeltedEvent */ + export interface ItemSmeltedEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { + smelting: net.minecraft.item.ItemStack; + } + } + } + + namespace net.minecraftforge.event { + /** net.minecraftforge.event.AnvilUpdateEvent */ + export interface AnvilUpdateEvent extends cpw.mods.fml.common.eventhandler.Event { + left: net.minecraft.item.ItemStack; + right: net.minecraft.item.ItemStack; + name: string; + output: net.minecraft.item.ItemStack; + cost: number; + materialCost: number; + } + + /** net.minecraftforge.event.CommandEvent */ + export interface CommandEvent extends cpw.mods.fml.common.eventhandler.Event { + command: any; + sender: any; + parameters: string[]; + exception: any; + } + + /** net.minecraftforge.event.ServerChatEvent */ + export interface ServerChatEvent extends cpw.mods.fml.common.eventhandler.Event { + message: string; + username: string; + player: net.minecraft.entity.player.EntityPlayer; + component: any; + } + + /** net.minecraftforge.event.FuelBurnTimeEvent */ + export interface FuelBurnTimeEvent extends cpw.mods.fml.common.eventhandler.Event { + fuel: net.minecraft.item.ItemStack; + burnTime: number; + } + + export namespace brewing { + /** net.minecraftforge.event.brewing.PotionBrewEvent */ + export interface PotionBrewEvent extends cpw.mods.fml.common.eventhandler.Event { + getItem(index: number): net.minecraft.item.ItemStack; + setItem(index: number, stack: net.minecraft.item.ItemStack): void; + getLength(): number; + } + export namespace PotionBrewEvent { + export type Pre = PotionBrewEvent + export type Post = PotionBrewEvent + } + /** net.minecraftforge.event.brewing.PotionBrewedEvent */ + export interface PotionBrewedEvent extends cpw.mods.fml.common.eventhandler.Event { + brewingStacks: net.minecraft.item.ItemStack[]; + } + } + + export namespace entity { + /** net.minecraftforge.event.entity.EntityEvent */ + export interface EntityEvent extends cpw.mods.fml.common.eventhandler.Event { + entity: net.minecraft.entity.Entity; + } + export namespace EntityEvent { + /** net.minecraftforge.event.entity.EntityEvent.EntityConstructing */ + export type EntityConstructing = net.minecraftforge.event.entity.EntityEvent + /** net.minecraftforge.event.entity.EntityEvent.CanUpdate */ + export interface CanUpdate extends net.minecraftforge.event.entity.EntityEvent { + canUpdate: boolean; + } + /** net.minecraftforge.event.entity.EntityEvent.EnteringChunk */ + export interface EnteringChunk extends net.minecraftforge.event.entity.EntityEvent { + newChunkX: number; + newChunkZ: number; + oldChunkX: number; + oldChunkZ: number; + } + } + + /** net.minecraftforge.event.entity.EntityJoinWorldEvent */ + export interface EntityJoinWorldEvent extends net.minecraftforge.event.entity.EntityEvent { + world: net.minecraft.world.World; + } + + /** net.minecraftforge.event.entity.EntityStruckByLightningEvent */ + export interface EntityStruckByLightningEvent extends net.minecraftforge.event.entity.EntityEvent { + lightning: net.minecraft.entity.Entity; + } + + /** net.minecraftforge.event.entity.PlaySoundAtEntityEvent */ + export interface PlaySoundAtEntityEvent extends net.minecraftforge.event.entity.EntityEvent { + name: string; + volume: number; + pitch: number; + } + + export namespace item { + /** net.minecraftforge.event.entity.item.ItemEvent */ + export interface ItemEvent extends net.minecraftforge.event.entity.EntityEvent { + entityItem: net.minecraft.entity.EntityItem; + } + /** net.minecraftforge.event.entity.item.ItemExpireEvent */ + export interface ItemExpireEvent extends net.minecraftforge.event.entity.item.ItemEvent { + extraLife: number; + } + /** net.minecraftforge.event.entity.item.ItemTossEvent */ + export interface ItemTossEvent extends net.minecraftforge.event.entity.item.ItemEvent { + player: net.minecraft.entity.player.EntityPlayer; + } + } + + export namespace living { + /** net.minecraftforge.event.entity.living.LivingEvent */ + export interface LivingEvent extends net.minecraftforge.event.entity.EntityEvent { + entityLiving: net.minecraft.entity.EntityLivingBase; + } + export namespace LivingEvent { + /** net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent */ + export type LivingUpdateEvent = net.minecraftforge.event.entity.living.LivingEvent + /** net.minecraftforge.event.entity.living.LivingEvent.LivingJumpEvent */ + export type LivingJumpEvent = net.minecraftforge.event.entity.living.LivingEvent + } + + /** net.minecraftforge.event.entity.living.LivingAttackEvent */ + export interface LivingAttackEvent extends net.minecraftforge.event.entity.living.LivingEvent { + source: net.minecraft.damage.DamageSource; + ammount: number; + } + + /** net.minecraftforge.event.entity.living.LivingDeathEvent */ + export interface LivingDeathEvent extends net.minecraftforge.event.entity.living.LivingEvent { + source: net.minecraft.damage.DamageSource; + } + + /** net.minecraftforge.event.entity.living.LivingDropsEvent */ + export interface LivingDropsEvent extends net.minecraftforge.event.entity.living.LivingEvent { + source: net.minecraft.damage.DamageSource; + drops: any[]; + lootingLevel: number; + recentlyHit: boolean; + specialDropValue: number; + } + + /** net.minecraftforge.event.entity.living.LivingFallEvent */ + export interface LivingFallEvent extends net.minecraftforge.event.entity.living.LivingEvent { + distance: number; + } + + /** net.minecraftforge.event.entity.living.LivingHealEvent */ + export interface LivingHealEvent extends net.minecraftforge.event.entity.living.LivingEvent { + amount: number; + } + + /** net.minecraftforge.event.entity.living.LivingHurtEvent */ + export interface LivingHurtEvent extends net.minecraftforge.event.entity.living.LivingEvent { + source: net.minecraft.damage.DamageSource; + ammount: number; + } + + /** net.minecraftforge.event.entity.living.LivingPackSizeEvent */ + export interface LivingPackSizeEvent extends net.minecraftforge.event.entity.living.LivingEvent { + maxPackSize: number; + } + + /** net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent */ + export interface LivingSetAttackTargetEvent extends net.minecraftforge.event.entity.living.LivingEvent { + target: net.minecraft.entity.EntityLivingBase; + } + + /** net.minecraftforge.event.entity.living.EnderTeleportEvent */ + export interface EnderTeleportEvent extends net.minecraftforge.event.entity.living.LivingEvent { + targetX: number; + targetY: number; + targetZ: number; + attackDamage: number; + } + + export namespace LivingSpawnEvent { + /** net.minecraftforge.event.entity.living.LivingSpawnEvent */ + export interface LivingSpawnEvent extends net.minecraftforge.event.entity.living.LivingEvent { + world: net.minecraft.world.World; + x: number; + y: number; + z: number; + } + /** net.minecraftforge.event.entity.living.LivingSpawnEvent.CheckSpawn */ + export type CheckSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.LivingSpawnEvent + /** net.minecraftforge.event.entity.living.LivingSpawnEvent.SpecialSpawn */ + export type SpecialSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.LivingSpawnEvent + /** net.minecraftforge.event.entity.living.LivingSpawnEvent.AllowDespawn */ + export type AllowDespawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.LivingSpawnEvent + } + + export namespace ZombieEvent { + /** net.minecraftforge.event.entity.living.ZombieEvent.SummonAidEvent */ + export interface SummonAidEvent extends net.minecraftforge.event.entity.EntityEvent { + customSummonedAid: net.minecraft.entity.EntityZombie; + world: net.minecraft.world.World; + x: number; + y: number; + z: number; + attacker: net.minecraft.entity.EntityLivingBase; + summonChance: number; + } + } + } + + export namespace player { + /** net.minecraftforge.event.entity.player.PlayerEvent */ + export interface PlayerEvent extends net.minecraftforge.event.entity.living.LivingEvent { + /** WARNING: Forge 1.7.10 uses 'entityPlayer' as the field name in this class! */ + entityPlayer: net.minecraft.entity.player.EntityPlayer; + } + export namespace PlayerEvent { + /** net.minecraftforge.event.entity.player.PlayerEvent.HarvestCheck */ + export interface HarvestCheck extends net.minecraftforge.event.entity.player.PlayerEvent { + block: net.minecraft.block.Block; + success: boolean; + } + /** net.minecraftforge.event.entity.player.PlayerEvent.BreakSpeed */ + export interface BreakSpeed extends net.minecraftforge.event.entity.player.PlayerEvent { + block: net.minecraft.block.Block; + metadata: number; + originalSpeed: number; + newSpeed: number; + x: number; + y: number; + z: number; + } + /** net.minecraftforge.event.entity.player.PlayerEvent.NameFormat */ + export interface NameFormat extends net.minecraftforge.event.entity.player.PlayerEvent { + username: string; + displayname: string; + } + /** net.minecraftforge.event.entity.player.PlayerEvent.Clone */ + export interface Clone extends net.minecraftforge.event.entity.player.PlayerEvent { + original: net.minecraft.entity.player.EntityPlayer; + wasDeath: boolean; + } + /** net.minecraftforge.event.entity.player.PlayerEvent.StartTracking */ + export interface StartTracking extends net.minecraftforge.event.entity.player.PlayerEvent { + target: net.minecraft.entity.Entity; + } + /** net.minecraftforge.event.entity.player.PlayerEvent.StopTracking */ + export interface StopTracking extends net.minecraftforge.event.entity.player.PlayerEvent { + target: net.minecraft.entity.Entity; + } + /** net.minecraftforge.event.entity.player.PlayerEvent.LoadFromFile */ + export interface LoadFromFile extends net.minecraftforge.event.entity.player.PlayerEvent { + playerDirectory: any; + playerUUID: string; + getPlayerFile(suffix: string): any; + } + /** net.minecraftforge.event.entity.player.PlayerEvent.SaveToFile */ + export interface SaveToFile extends net.minecraftforge.event.entity.player.PlayerEvent { + playerDirectory: any; + playerUUID: string; + getPlayerFile(suffix: string): any; + } + } + + /** net.minecraftforge.event.entity.player.AchievementEvent */ + export interface AchievementEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + achievement: any; + } + + /** net.minecraftforge.event.entity.player.AnvilRepairEvent */ + export interface AnvilRepairEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + left: net.minecraft.item.ItemStack; + right: net.minecraft.item.ItemStack; + output: net.minecraft.item.ItemStack; + } + + /** net.minecraftforge.event.entity.player.ArrowLooseEvent */ + export interface ArrowLooseEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + bow: net.minecraft.item.ItemStack; + charge: number; + } + + /** net.minecraftforge.event.entity.player.ArrowNockEvent */ + export interface ArrowNockEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + result: net.minecraft.item.ItemStack; + } + + /** net.minecraftforge.event.entity.player.AttackEntityEvent */ + export interface AttackEntityEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + target: net.minecraft.entity.Entity; + } + + /** net.minecraftforge.event.entity.player.BonemealEvent */ + export interface BonemealEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + world: net.minecraft.world.World; + block: net.minecraft.block.Block; + x: number; + y: number; + z: number; + } + + /** net.minecraftforge.event.entity.player.EntityInteractEvent */ + export interface EntityInteractEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + target: net.minecraft.entity.Entity; + } + + /** net.minecraftforge.event.entity.player.EntityItemPickupEvent */ + export interface EntityItemPickupEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + item: net.minecraft.entity.EntityItem; + } + + /** net.minecraftforge.event.entity.player.FillBucketEvent */ + export interface FillBucketEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + current: net.minecraft.item.ItemStack; + result: net.minecraft.item.ItemStack; + world: net.minecraft.world.World; + target: net.minecraft.util.MovingObjectPosition; + } + + /** net.minecraftforge.event.entity.player.ItemTooltipEvent */ + export interface ItemTooltipEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + showAdvancedItemTooltips: boolean; + itemStack: net.minecraft.item.ItemStack; + toolTip: string[]; + } + + /** net.minecraftforge.event.entity.player.PlayerDestroyItemEvent */ + export interface PlayerDestroyItemEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + original: net.minecraft.item.ItemStack; + } + + /** net.minecraftforge.event.entity.player.PlayerDropsEvent */ + export interface PlayerDropsEvent extends net.minecraftforge.event.entity.living.LivingDropsEvent { + entityPlayer: net.minecraft.entity.player.EntityPlayer; + } + + /** net.minecraftforge.event.entity.player.PlayerFlyableFallEvent */ + export interface PlayerFlyableFallEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + distance: number; + } + + /** net.minecraftforge.event.entity.player.PlayerInteractEvent */ + export interface PlayerInteractEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + action: 'RIGHT_CLICK_AIR' | 'RIGHT_CLICK_BLOCK' | 'LEFT_CLICK_BLOCK'; + x: number; + y: number; + z: number; + face: number; + world: net.minecraft.world.World; + useBlock: 'ALLOW' | 'DENY' | 'DEFAULT'; + useItem: 'ALLOW' | 'DENY' | 'DEFAULT'; + } + + /** net.minecraftforge.event.entity.player.PlayerOpenContainerEvent */ + export interface PlayerOpenContainerEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + canInteractWith: boolean; + } + + /** net.minecraftforge.event.entity.player.PlayerPickupXpEvent */ + export interface PlayerPickupXpEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + orb: any; + } + + /** net.minecraftforge.event.entity.player.PlayerSleepInBedEvent */ + export interface PlayerSleepInBedEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + x: number; + y: number; + z: number; + result: 'OK' | 'NOT_POSSIBLE_HERE' | 'NOT_POSSIBLE_NOW' | 'TOO_FAR_AWAY' | 'OTHER_PROBLEM' | 'NOT_SAFE'; + } + + /** net.minecraftforge.event.entity.player.PlayerUseItemEvent */ + export interface PlayerUseItemEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + item: net.minecraft.item.ItemStack; + duration: number; + } + export namespace PlayerUseItemEvent { + export type Start = net.minecraftforge.event.entity.player.PlayerUseItemEvent + export type Tick = net.minecraftforge.event.entity.player.PlayerUseItemEvent + export type Stop = net.minecraftforge.event.entity.player.PlayerUseItemEvent + export type Finish = net.minecraftforge.event.entity.player.PlayerUseItemEvent + } + + /** net.minecraftforge.event.entity.player.PlayerWakeUpEvent */ + export interface PlayerWakeUpEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + wakeImmediately: boolean; + updateWorld: boolean; + setSpawn: boolean; + } + + /** net.minecraftforge.event.entity.player.UseHoeEvent */ + export interface UseHoeEvent extends net.minecraftforge.event.entity.player.PlayerEvent { + current: net.minecraft.item.ItemStack; + world: net.minecraft.world.World; + x: number; + y: number; + z: number; + } + } + } + + export namespace world { + /** net.minecraftforge.event.world.WorldEvent */ + export interface WorldEvent extends cpw.mods.fml.common.eventhandler.Event { + world: net.minecraft.world.World; + } + export namespace WorldEvent { + export type Load = net.minecraftforge.event.world.WorldEvent + export type Unload = net.minecraftforge.event.world.WorldEvent + export type Save = net.minecraftforge.event.world.WorldEvent + export interface PotentialSpawns extends net.minecraftforge.event.world.WorldEvent { + type: any; + x: number; + y: number; + z: number; + list: any[]; + } + export interface CreateSpawnPosition extends net.minecraftforge.event.world.WorldEvent { + settings: any; + } + } + + export namespace BlockEvent { + /** net.minecraftforge.event.world.BlockEvent */ + export interface BlockEvent extends cpw.mods.fml.common.eventhandler.Event { + x: number; + y: number; + z: number; + world: net.minecraft.world.World; + block: net.minecraft.block.Block; + blockMetadata: number; + } + /** net.minecraftforge.event.world.BlockEvent.BreakEvent */ + export interface BreakEvent extends net.minecraftforge.event.world.BlockEvent.BlockEvent { + /** @returns The player who broke the block */ + getPlayer(): net.minecraft.entity.player.EntityPlayer; + /** @returns The experience to drop */ + getExpToDrop(): number; + /** Sets the experience to drop */ + setExpToDrop(exp: number): void; + } + /** net.minecraftforge.event.world.BlockEvent.HarvestDropsEvent */ + export interface HarvestDropsEvent extends net.minecraftforge.event.world.BlockEvent.BlockEvent { + fortuneLevel: number; + drops: net.minecraft.item.ItemStack[]; + isSilkTouching: boolean; + dropChance: number; + /** May be null for non-player harvesting */ + harvester: net.minecraft.entity.player.EntityPlayer; + } + /** net.minecraftforge.event.world.BlockEvent.PlaceEvent */ + export interface PlaceEvent extends net.minecraftforge.event.world.BlockEvent.BlockEvent { + player: net.minecraft.entity.player.EntityPlayer; + itemInHand: net.minecraft.item.ItemStack; + placedBlock: net.minecraft.block.Block; + placedAgainst: net.minecraft.block.Block; + /** Note: BlockSnapshot is a complex structure, simplifying here */ + blockSnapshot: any; + } + /** net.minecraftforge.event.world.BlockEvent.MultiPlaceEvent */ + export interface MultiPlaceEvent extends net.minecraftforge.event.world.BlockEvent.PlaceEvent { + /** @returns A list of replaced block snapshots */ + getReplacedBlockSnapshots(): any[]; + } + } + + export namespace explosion { + /** net.minecraftforge.event.world.ExplosionEvent */ + export interface ExplosionEvent extends cpw.mods.fml.common.eventhandler.Event { + world: net.minecraft.world.World; + explosion: any; + } + export namespace ExplosionEvent { + export type Start = net.minecraftforge.event.world.explosion.ExplosionEvent + export interface Detonate extends net.minecraftforge.event.world.explosion.ExplosionEvent { + affectedEntities: net.minecraft.entity.Entity[]; + } + } + } + + export namespace note { + /** net.minecraftforge.event.world.NoteBlockEvent */ + export type NoteBlockEvent = net.minecraftforge.event.world.BlockEvent.BlockEvent + export namespace NoteBlockEvent { + export interface Play extends net.minecraftforge.event.world.note.NoteBlockEvent { + instrument: any; + note: number; + } + export interface Change extends net.minecraftforge.event.world.note.NoteBlockEvent { + oldInstrument: any; + oldNote: number; + newInstrument: any; + newNote: number; + } + } + } + } + } + + // ============================================================================ + // GLOBAL ALIASES - To match CustomNPC+ pattern (e.g., IForgeEvent.AnvilUpdateEvent) + // Points to the new deep structure to ensure correct tooltips. + // ============================================================================ + namespace IForgeEvent { + export type InitEvent = cpw.mods.fml.common.eventhandler.Event; + + export type AnvilUpdateEvent = net.minecraftforge.event.AnvilUpdateEvent; + export type CommandEvent = net.minecraftforge.event.CommandEvent; + export type ServerChatEvent = net.minecraftforge.event.ServerChatEvent; + export type FuelBurnTimeEvent = net.minecraftforge.event.FuelBurnTimeEvent; + + export type PotionBrewEvent = net.minecraftforge.event.brewing.PotionBrewEvent; + export type PotionBrewedEvent = net.minecraftforge.event.brewing.PotionBrewedEvent; + + export type EntityConstructing = net.minecraftforge.event.entity.EntityEvent.EntityConstructing; + export type CanUpdate = net.minecraftforge.event.entity.EntityEvent.CanUpdate; + export type EnteringChunk = net.minecraftforge.event.entity.EntityEvent.EnteringChunk; + export type EntityJoinWorldEvent = net.minecraftforge.event.entity.EntityJoinWorldEvent; + export type EntityStruckByLightningEvent = net.minecraftforge.event.entity.EntityStruckByLightningEvent; + export type PlaySoundAtEntityEvent = net.minecraftforge.event.entity.PlaySoundAtEntityEvent; + + export type ItemEvent = net.minecraftforge.event.entity.item.ItemEvent; + export type ItemExpireEvent = net.minecraftforge.event.entity.item.ItemExpireEvent; + export type ItemTossEvent = net.minecraftforge.event.entity.item.ItemTossEvent; + + export type LivingUpdateEvent = net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent; + export type LivingJumpEvent = net.minecraftforge.event.entity.living.LivingEvent.LivingJumpEvent; + export type LivingAttackEvent = net.minecraftforge.event.entity.living.LivingAttackEvent; + export type LivingDeathEvent = net.minecraftforge.event.entity.living.LivingDeathEvent; + export type LivingDropsEvent = net.minecraftforge.event.entity.living.LivingDropsEvent; + export type LivingFallEvent = net.minecraftforge.event.entity.living.LivingFallEvent; + export type LivingHealEvent = net.minecraftforge.event.entity.living.LivingHealEvent; + export type LivingHurtEvent = net.minecraftforge.event.entity.living.LivingHurtEvent; + export type LivingPackSizeEvent = net.minecraftforge.event.entity.living.LivingPackSizeEvent; + export type LivingSetAttackTargetEvent = net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent; + export type EnderTeleportEvent = net.minecraftforge.event.entity.living.EnderTeleportEvent; + + export type CheckSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.CheckSpawn; + export type SpecialSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.SpecialSpawn; + export type AllowDespawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.AllowDespawn; + export type SummonAidEvent = net.minecraftforge.event.entity.living.ZombieEvent.SummonAidEvent; + + export type HarvestCheck = net.minecraftforge.event.entity.player.PlayerEvent.HarvestCheck; + export type BreakSpeed = net.minecraftforge.event.entity.player.PlayerEvent.BreakSpeed; + export type NameFormat = net.minecraftforge.event.entity.player.PlayerEvent.NameFormat; + export type Clone = net.minecraftforge.event.entity.player.PlayerEvent.Clone; + export type StartTracking = net.minecraftforge.event.entity.player.PlayerEvent.StartTracking; + export type StopTracking = net.minecraftforge.event.entity.player.PlayerEvent.StopTracking; + export type LoadFromFile = net.minecraftforge.event.entity.player.PlayerEvent.LoadFromFile; + export type SaveToFile = net.minecraftforge.event.entity.player.PlayerEvent.SaveToFile; + + export type AchievementEvent = net.minecraftforge.event.entity.player.AchievementEvent; + export type AnvilRepairEvent = net.minecraftforge.event.entity.player.AnvilRepairEvent; + export type ArrowLooseEvent = net.minecraftforge.event.entity.player.ArrowLooseEvent; + export type ArrowNockEvent = net.minecraftforge.event.entity.player.ArrowNockEvent; + export type AttackEntityEvent = net.minecraftforge.event.entity.player.AttackEntityEvent; + export type BonemealEvent = net.minecraftforge.event.entity.player.BonemealEvent; + export type EntityInteractEvent = net.minecraftforge.event.entity.player.EntityInteractEvent; + export type EntityItemPickupEvent = net.minecraftforge.event.entity.player.EntityItemPickupEvent; + export type FillBucketEvent = net.minecraftforge.event.entity.player.FillBucketEvent; + export type ItemTooltipEvent = net.minecraftforge.event.entity.player.ItemTooltipEvent; + export type PlayerDestroyItemEvent = net.minecraftforge.event.entity.player.PlayerDestroyItemEvent; + export type PlayerDropsEvent = net.minecraftforge.event.entity.player.PlayerDropsEvent; + export type PlayerFlyableFallEvent = net.minecraftforge.event.entity.player.PlayerFlyableFallEvent; + export type PlayerInteractEvent = net.minecraftforge.event.entity.player.PlayerInteractEvent; + export type PlayerOpenContainerEvent = net.minecraftforge.event.entity.player.PlayerOpenContainerEvent; + export type PlayerPickupXpEvent = net.minecraftforge.event.entity.player.PlayerPickupXpEvent; + export type PlayerSleepInBedEvent = net.minecraftforge.event.entity.player.PlayerSleepInBedEvent; + export type UseHoeEvent = net.minecraftforge.event.entity.player.UseHoeEvent; + + export type PlayerUseItemStart = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Start; + export type PlayerUseItemTick = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Tick; + export type PlayerUseItemStop = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Stop; + export type PlayerUseItemFinish = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Finish; + export type PlayerWakeUpEvent = net.minecraftforge.event.entity.player.PlayerWakeUpEvent; + + export type WorldLoad = net.minecraftforge.event.world.WorldEvent.Load; + export type WorldUnload = net.minecraftforge.event.world.WorldEvent.Unload; + export type WorldSave = net.minecraftforge.event.world.WorldEvent.Save; + export type PotentialSpawns = net.minecraftforge.event.world.WorldEvent.PotentialSpawns; + export type CreateSpawnPosition = net.minecraftforge.event.world.WorldEvent.CreateSpawnPosition; + + export type BreakEvent = net.minecraftforge.event.world.BlockEvent.BreakEvent; + export type HarvestDropsEvent = net.minecraftforge.event.world.BlockEvent.HarvestDropsEvent; + export type PlaceEvent = net.minecraftforge.event.world.BlockEvent.PlaceEvent; + export type MultiPlaceEvent = net.minecraftforge.event.world.BlockEvent.MultiPlaceEvent; + export type BlockEvent = net.minecraftforge.event.world.BlockEvent.BlockEvent; + + export type ExplosionStart = net.minecraftforge.event.world.explosion.ExplosionEvent.Start; + export type ExplosionDetonate = net.minecraftforge.event.world.explosion.ExplosionEvent.Detonate; + + export type NotePlay = net.minecraftforge.event.world.note.NoteBlockEvent.Play; + export type NoteChange = net.minecraftforge.event.world.note.NoteBlockEvent.Change; + + // FML Events + export type PlayerTickEvent = cpw.mods.fml.common.gameevent.TickEvent.PlayerTickEvent; + export type WorldTickEvent = cpw.mods.fml.common.gameevent.TickEvent.WorldTickEvent; + export type ServerTickEvent = cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent; + export type ClientTickEvent = cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent; + export type RenderTickEvent = cpw.mods.fml.common.gameevent.TickEvent.RenderTickEvent; + export type TickEvent = cpw.mods.fml.common.gameevent.TickEvent; + + export type KeyInputEvent = cpw.mods.fml.common.gameevent.InputEvent.KeyInputEvent; + export type MouseInputEvent = cpw.mods.fml.common.gameevent.InputEvent.MouseInputEvent; + + export type PlayerLoggedInEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent; + export type PlayerLoggedOutEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent; + export type PlayerRespawnEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerRespawnEvent; + export type PlayerChangedDimensionEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerChangedDimensionEvent; + export type PlayerItemPickupEvent = cpw.mods.fml.common.gameevent.PlayerEvent.ItemPickupEvent; + export type PlayerItemCraftedEvent = cpw.mods.fml.common.gameevent.PlayerEvent.ItemCraftedEvent; + export type PlayerItemSmeltedEvent = cpw.mods.fml.common.gameevent.PlayerEvent.ItemSmeltedEvent; + } +} + +export { }; diff --git a/src/main/resources/assets/customnpcs/api/hooks.d.ts b/src/main/resources/assets/customnpcs/api/hooks.d.ts new file mode 100644 index 000000000..3a5811ab8 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/hooks.d.ts @@ -0,0 +1,259 @@ +import './minecraft-raw.d.ts'; +import './forge-events-raw.d.ts'; + +/** + * CustomNPC+ Event Hook Overloads + * + * Functions are grouped by name to enable precise type inference. + * For common hooks (init, tick, damaged), parameter names (INpcEvent, IPlayerEvent, etc.) + * are used to help the LSP distinguish between multiple valid overloads. + */ + +declare global { + // ============ Global Lifecycle & Common Hooks ============ + function init(INpcEvent: INpcEvent.InitEvent): void; + function init(IPlayerEvent: IPlayerEvent.InitEvent): void; + function init(IItemEvent: IItemEvent.InitEvent): void; + function init(IBlockEvent: IBlockEvent.InitEvent): void; + function init(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; + + function tick(INpcEvent: INpcEvent.UpdateEvent): void; + function tick(IPlayerEvent: IPlayerEvent.UpdateEvent): void; + function tick(IItemEvent: IItemEvent.UpdateEvent): void; + function tick(IBlockEvent: IBlockEvent.UpdateEvent): void; + + function interact(INpcEvent: INpcEvent.InteractEvent): void; + function interact(IPlayerEvent: IPlayerEvent.InteractEvent): void; + function interact(IItemEvent: IItemEvent.InteractEvent): void; + function interact(IBlockEvent: IBlockEvent.InteractEvent): void; + + function rightClick(IPlayerEvent: IPlayerEvent.RightClickEvent): void; + function rightClick(IItemEvent: IItemEvent.RightClickEvent): void; + + function attack(IPlayerEvent: IPlayerEvent.AttackEvent): void; + function attack(IItemEvent: IItemEvent.AttackEvent): void; + + function damaged(INpcEvent: INpcEvent.DamagedEvent): void; + function damaged(IPlayerEvent: IPlayerEvent.DamagedEvent): void; + function damaged(ForgeEvent: net.minecraftforge.event.entity.living.LivingHurtEvent): void; + + function killed(INpcEvent: INpcEvent.DiedEvent): void; + function killed(IPlayerEvent: IPlayerEvent.DiedEvent): void; + + function kills(INpcEvent: INpcEvent.KilledEntityEvent): void; + function kills(IPlayerEvent: IPlayerEvent.KilledEntityEvent): void; + + function timer(INpcEvent: INpcEvent.TimerEvent): void; + function timer(IPlayerEvent: IPlayerEvent.TimerEvent): void; + function timer(IBlockEvent: IBlockEvent.TimerEvent): void; + + function dialogClose(INpcEvent: INpcEvent.DialogClosedEvent): void; + function dialogClose(IDialogEvent: IDialogEvent.DialogClosed): void; + + function rangedLaunched(INpcEvent: INpcEvent.RangedLaunchedEvent): void; + function rangedLaunched(IPlayerEvent: IPlayerEvent.RangedLaunchedEvent): void; + + function startItem(IPlayerEvent: IPlayerEvent.StartUsingItem): void; + function startItem(IItemEvent: IItemEvent.StartUsingItem): void; + + function usingItem(IPlayerEvent: IPlayerEvent.UsingItem): void; + function usingItem(IItemEvent: IItemEvent.UsingItem): void; + + function stopItem(IPlayerEvent: IPlayerEvent.StopUsingItem): void; + function stopItem(IItemEvent: IItemEvent.StopUsingItem): void; + + function finishItem(IPlayerEvent: IPlayerEvent.FinishUsingItem): void; + function finishItem(IItemEvent: IItemEvent.FinishUsingItem): void; + + // ============ Player Specific Hooks ============ + function attacked(IPlayerEvent: IPlayerEvent.AttackedEvent): void; + function damagedEntity(IPlayerEvent: IPlayerEvent.DamagedEntityEvent): void; + function drop(IPlayerEvent: IPlayerEvent.DropEvent): void; + function respawn(IPlayerEvent: IPlayerEvent.RespawnEvent): void; + function breakBlock(IPlayerEvent: IPlayerEvent.BreakEvent): void; + function chat(IPlayerEvent: IPlayerEvent.ChatEvent): void; + function login(IPlayerEvent: IPlayerEvent.LoginEvent): void; + function logout(IPlayerEvent: IPlayerEvent.LogoutEvent): void; + function keyPressed(IPlayerEvent: IPlayerEvent.KeyPressedEvent): void; + function mouseClicked(IPlayerEvent: IPlayerEvent.MouseClickedEvent): void; + function toss(IPlayerEvent: IPlayerEvent.TossEvent): void; + function pickUp(IPlayerEvent: IPlayerEvent.PickUpEvent): void; + function pickupXP(IPlayerEvent: IPlayerEvent.PickupXPEvent): void; + function rangedCharge(IPlayerEvent: IPlayerEvent.RangedChargeEvent): void; + function containerOpen(IPlayerEvent: IPlayerEvent.ContainerOpen): void; + function useHoe(IPlayerEvent: IPlayerEvent.UseHoeEvent): void; + function bonemeal(IPlayerEvent: IPlayerEvent.BonemealEvent): void; + function fillBucket(IPlayerEvent: IPlayerEvent.FillBucketEvent): void; + function jump(IPlayerEvent: IPlayerEvent.JumpEvent): void; + function fall(IPlayerEvent: IPlayerEvent.FallEvent): void; + function wakeUp(IPlayerEvent: IPlayerEvent.WakeUpEvent): void; + function sleep(IPlayerEvent: IPlayerEvent.SleepEvent): void; + function playSound(IPlayerEvent: IPlayerEvent.SoundEvent): void; + function lightning(IPlayerEvent: IPlayerEvent.LightningEvent): void; + function changedDim(IPlayerEvent: IPlayerEvent.ChangedDimension): void; + + // ============ NPC Specific Hooks ============ + function dialog(INpcEvent: INpcEvent.DialogEvent): void; + function meleeAttack(INpcEvent: INpcEvent.MeleeAttackEvent): void; + function meleeSwing(INpcEvent: INpcEvent.SwingEvent): void; + function target(INpcEvent: INpcEvent.TargetEvent): void; + function collide(INpcEvent: INpcEvent.CollideEvent): void; + function targetLost(INpcEvent: INpcEvent.TargetLostEvent): void; + + // ============ Item Specific Hooks ============ + function tossed(IItemEvent: IItemEvent.TossedEvent): void; + function pickedUp(IItemEvent: IItemEvent.PickedUpEvent): void; + function spawn(IItemEvent: IItemEvent.SpawnEvent): void; + + // ============ Projectile Specific Hooks ============ + function projectileTick(IProjectileEvent: IProjectileEvent.UpdateEvent): void; + function projectileImpact(IProjectileEvent: IProjectileEvent.ImpactEvent): void; + + // ============ Block Specific Hooks ============ + function redstone(IBlockEvent: IBlockEvent.RedstoneEvent): void; + function broken(IBlockEvent: IBlockEvent.BreakEvent): void; + function exploded(IBlockEvent: IBlockEvent.ExplodedEvent): void; + function rainFilled(IBlockEvent: IBlockEvent.RainFillEvent): void; + function neighborChanged(IBlockEvent: IBlockEvent.NeighborChangedEvent): void; + function clicked(IBlockEvent: IBlockEvent.ClickedEvent): void; + function harvested(IBlockEvent: IBlockEvent.HarvestedEvent): void; + function collide(IBlockEvent: IBlockEvent.CollidedEvent): void; + function fallenUpon(IBlockEvent: IBlockEvent.EntityFallenUponEvent): void; + + // ============ Specialized Systems (Quest, Dialog, GUI, etc.) ============ + function questStart(IQuestEvent: IQuestEvent.QuestStartEvent): void; + function questCompleted(IQuestEvent: IQuestEvent.QuestCompletedEvent): void; + function questTurnIn(IQuestEvent: IQuestEvent.QuestTurnedInEvent): void; + function factionPoints(IFactionEvent: IFactionEvent.FactionPointsEvent): void; + function dialogOpen(IDialogEvent: IDialogEvent.DialogClosed): void; + function dialogOption(IDialogEvent: IDialogEvent.DialogOption): void; + function customGuiClosed(ICustomGuiEvent: ICustomGuiEvent.CloseEvent): void; + function customGuiButton(ICustomGuiEvent: ICustomGuiEvent.ButtonEvent): void; + function customGuiSlot(ICustomGuiEvent: ICustomGuiEvent.SlotEvent): void; + function customGuiSlotClicked(ICustomGuiEvent: ICustomGuiEvent.SlotClickEvent): void; + function customGuiScroll(ICustomGuiEvent: ICustomGuiEvent.ScrollEvent): void; + + // ============ DBC Addon Hooks ============ + function dbcFormChange(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.FormChangeEvent): void; + function dbcDamaged(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.DamagedEvent): void; + function dbcCapsuleUsed(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.CapsuleUsedEvent): void; + function dbcSenzuUsed(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.SenzuUsedEvent): void; + function dbcRevived(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.DBCReviveEvent): void; + function dbcKnockout(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.DBCKnockout): void; + + // ============ Forge & Minecraft RAW Hooks ============ + function onCNPCNaturalSpawn(ICustomNPCsEvent: ICustomNPCsEvent.CNPCNaturalSpawnEvent): void; + + function anvilUpdateEvent(ForgeEvent: net.minecraftforge.event.AnvilUpdateEvent): void; + function tickEventPlayerTickEvent(ForgeEvent: cpw.mods.fml.common.gameevent.TickEvent.PlayerTickEvent): void; + function tickEventWorldTickEvent(ForgeEvent: cpw.mods.fml.common.gameevent.TickEvent.WorldTickEvent): void; + function tickEventServerTickEvent(ForgeEvent: cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent): void; + function commandEvent(ForgeEvent: net.minecraftforge.event.CommandEvent): void; + function serverChatEvent(ForgeEvent: net.minecraftforge.event.ServerChatEvent): void; + function itemTossEvent(ForgeEvent: net.minecraftforge.event.entity.item.ItemTossEvent): void; + function livingHurtEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingHurtEvent): void; + function fillBucketEvent(ForgeEvent: net.minecraftforge.event.entity.player.FillBucketEvent): void; + + function inputEventKeyInputEvent(ForgeEvent: cpw.mods.fml.common.gameevent.InputEvent.KeyInputEvent): void; + function inputEventMouseInputEvent(ForgeEvent: cpw.mods.fml.common.gameevent.InputEvent.MouseInputEvent): void; + function playerEventPlayerChangedDimensionEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerChangedDimensionEvent): void; + function playerEventPlayerRespawnEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerRespawnEvent): void; + function playerEventPlayerLoggedOutEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent): void; + function playerEventPlayerLoggedInEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent): void; + function playerEventItemSmeltedEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.ItemSmeltedEvent): void; + function playerEventItemCraftedEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.ItemCraftedEvent): void; + function playerEventItemPickupEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.ItemPickupEvent): void; + function fuelBurnTimeEvent(ForgeEvent: net.minecraftforge.event.FuelBurnTimeEvent): void; + function potionBrewEventPost(ForgeEvent: net.minecraftforge.event.brewing.PotionBrewEvent.Post): void; + function potionBrewEventPre(ForgeEvent: net.minecraftforge.event.brewing.PotionBrewEvent.Pre): void; + function potionBrewedEvent(ForgeEvent: net.minecraftforge.event.brewing.PotionBrewedEvent): void; + function entityEventEnteringChunk(ForgeEvent: net.minecraftforge.event.entity.EntityEvent.EnteringChunk): void; + function entityEventCanUpdate(ForgeEvent: net.minecraftforge.event.entity.EntityEvent.CanUpdate): void; + function entityJoinWorldEvent(ForgeEvent: net.minecraftforge.event.entity.EntityJoinWorldEvent): void; + function entityStruckByLightningEvent(ForgeEvent: net.minecraftforge.event.entity.EntityStruckByLightningEvent): void; + function playSoundAtEntityEvent(ForgeEvent: net.minecraftforge.event.entity.PlaySoundAtEntityEvent): void; + function itemEvent(ForgeEvent: net.minecraftforge.event.entity.item.ItemEvent): void; + function itemExpireEvent(ForgeEvent: net.minecraftforge.event.entity.item.ItemExpireEvent): void; + function enderTeleportEvent(ForgeEvent: net.minecraftforge.event.entity.living.EnderTeleportEvent): void; + function livingAttackEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingAttackEvent): void; + function livingDeathEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingDeathEvent): void; + function livingDropsEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingDropsEvent): void; + function livingEventLivingJumpEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingEvent.LivingJumpEvent): void; + function livingFallEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingFallEvent): void; + function livingHealEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingHealEvent): void; + function livingPackSizeEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingPackSizeEvent): void; + function livingSetAttackTargetEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent): void; + function livingSpawnEventAllowDespawn(ForgeEvent: net.minecraftforge.event.entity.living.LivingSpawnEvent.AllowDespawn): void; + function livingSpawnEventSpecialSpawn(ForgeEvent: net.minecraftforge.event.entity.living.LivingSpawnEvent.SpecialSpawn): void; + function livingSpawnEventCheckSpawn(ForgeEvent: net.minecraftforge.event.entity.living.LivingSpawnEvent.CheckSpawn): void; + function zombieEventSummonAidEvent(ForgeEvent: net.minecraftforge.event.entity.living.ZombieEvent.SummonAidEvent): void; + function minecartCollisionEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; + function minecartEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; + function minecartInteractEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; + function minecartUpdateEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; + function achievementEvent(ForgeEvent: net.minecraftforge.event.entity.player.AchievementEvent): void; + function anvilRepairEvent(ForgeEvent: net.minecraftforge.event.entity.player.AnvilRepairEvent): void; + function arrowLooseEvent(ForgeEvent: net.minecraftforge.event.entity.player.ArrowLooseEvent): void; + function arrowNockEvent(ForgeEvent: net.minecraftforge.event.entity.player.ArrowNockEvent): void; + function attackEntityEvent(ForgeEvent: net.minecraftforge.event.entity.player.AttackEntityEvent): void; + function bonemealEvent(ForgeEvent: net.minecraftforge.event.entity.player.BonemealEvent): void; + function entityInteractEvent(ForgeEvent: net.minecraftforge.event.entity.player.EntityInteractEvent): void; + function entityItemPickupEvent(ForgeEvent: net.minecraftforge.event.entity.player.EntityItemPickupEvent): void; + function playerDestroyItemEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerDestroyItemEvent): void; + function playerDropsEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerDropsEvent): void; + function playerEventSaveToFile(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.SaveToFile): void; + function playerEventLoadFromFile(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.LoadFromFile): void; + function playerEventStopTracking(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.StopTracking): void; + function playerEventStartTracking(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.StartTracking): void; + function playerEventClone(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.Clone): void; + function playerEventNameFormat(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.NameFormat): void; + function playerEventBreakSpeed(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.BreakSpeed): void; + function playerEventHarvestCheck(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.HarvestCheck): void; + function playerFlyableFallEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerFlyableFallEvent): void; + function playerOpenContainerEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerOpenContainerEvent): void; + function playerPickupXpEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerPickupXpEvent): void; + function playerSleepInBedEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerSleepInBedEvent): void; + function playerUseItemEventFinish(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Finish): void; + function playerUseItemEventStop(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Stop): void; + function playerUseItemEventTick(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Tick): void; + function playerUseItemEventStart(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Start): void; + function playerWakeUpEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerWakeUpEvent): void; + function useHoeEvent(ForgeEvent: net.minecraftforge.event.entity.player.UseHoeEvent): void; + function blockEventMultiPlaceEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.MultiPlaceEvent): void; + function blockEventPlaceEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.PlaceEvent): void; + function blockEventBreakEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.BreakEvent): void; + function blockEventHarvestDropsEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.HarvestDropsEvent): void; + function explosionEventDetonate(ForgeEvent: net.minecraftforge.event.world.explosion.ExplosionEvent.Detonate): void; + function explosionEventStart(ForgeEvent: net.minecraftforge.event.world.explosion.ExplosionEvent.Start): void; + function noteBlockEventChange(ForgeEvent: net.minecraftforge.event.world.note.NoteBlockEvent.Change): void; + function noteBlockEventPlay(ForgeEvent: net.minecraftforge.event.world.note.NoteBlockEvent.Play): void; + function worldEventCreateSpawnPosition(ForgeEvent: net.minecraftforge.event.world.WorldEvent.CreateSpawnPosition): void; + function worldEventSave(ForgeEvent: net.minecraftforge.event.world.WorldEvent.Save): void; + function worldEventUnload(ForgeEvent: net.minecraftforge.event.world.WorldEvent.Unload): void; + function worldEventLoad(ForgeEvent: net.minecraftforge.event.world.WorldEvent.Load): void; + + // ============ Other Systems ============ + function scriptCommand(ICustomNPCsEvent: ICustomNPCsEvent.ScriptedCommandEvent): void; + + function partyQuestCompleted(IPartyEvent: IPartyEvent.PartyQuestCompletedEvent): void; + function partyQuestSet(IPartyEvent: IPartyEvent.PartyQuestSetEvent): void; + function partyQuestTurnedIn(IPartyEvent: IPartyEvent.PartyQuestTurnedInEvent): void; + function partyInvite(IPartyEvent: IPartyEvent.PartyInviteEvent): void; + function partyKick(IPartyEvent: IPartyEvent.PartyKickEvent): void; + function partyLeave(IPartyEvent: IPartyEvent.PartyLeaveEvent): void; + function partyDisband(IPartyEvent: IPartyEvent.PartyDisbandEvent): void; + + function animationStart(IAnimationEvent: IAnimationEvent.Started): void; + function animationEnd(IAnimationEvent: IAnimationEvent.Ended): void; + function frameEnter(IAnimationEvent: IAnimationEvent.IFrameEvent): void; + function frameExit(IAnimationEvent: IAnimationEvent.IFrameEvent): void; + + function profileChange(IPlayerEvent: IPlayerEvent.ProfileEvent.Changed): void; + function profileRemove(IPlayerEvent: IPlayerEvent.ProfileEvent.Removed): void; + function profileCreate(IPlayerEvent: IPlayerEvent.ProfileEvent.Create): void; + function onEffectAdd(IPlayerEvent: IPlayerEvent.EffectEvent.Added): void; + function onEffectTick(IPlayerEvent: IPlayerEvent.EffectEvent.Ticked): void; + function onEffectRemove(IPlayerEvent: IPlayerEvent.EffectEvent.Removed): void; +} + +export { }; diff --git a/src/main/resources/assets/customnpcs/api/index.d.ts b/src/main/resources/assets/customnpcs/api/index.d.ts new file mode 100644 index 000000000..43c76fc9d --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/index.d.ts @@ -0,0 +1,303 @@ +/** + * Centralized global declarations for CustomNPC+ scripting in VSCode. + * + * This file combines: + * 1. Type aliases for all generated interfaces (so you can use INpcEvent, IPlayer, etc. without imports) + * 2. Ambient function declarations for handler names (so VSCode infers the event type by function name) + * + * Include this file in jsconfig.json so all .js scripts see these globals without per-file imports. + */ + +declare global { + // ============================================================================ + // TYPE ALIASES - Make all interfaces available globally in JS scripts + // ============================================================================ + + type AbstractNpcAPI = import('./noppes/npcs/api/AbstractNpcAPI').AbstractNpcAPI; + type BlockPos = import('./net/minecraft/util/math/BlockPos').BlockPos; + type IAction = import('./noppes/npcs/api/handler/data/IAction').IAction; + type IActionChain = import('./noppes/npcs/api/handler/data/IActionChain').IActionChain; + type IActionListener = import('./noppes/npcs/api/handler/data/IActionListener').IActionListener; + type IActionManager = import('./noppes/npcs/api/handler/IActionManager').IActionManager; + type IActionQueue = import('./noppes/npcs/api/handler/data/IActionQueue').IActionQueue; + type IAnimal = import('./noppes/npcs/api/entity/IAnimal').IAnimal; + type IAnimatable = import('./noppes/npcs/api/entity/IAnimatable').IAnimatable; + type IAnimation = import('./noppes/npcs/api/handler/data/IAnimation').IAnimation; + type IAnimationData = import('./noppes/npcs/api/handler/data/IAnimationData').IAnimationData; + type IAnimationEvent = import('./noppes/npcs/api/event/IAnimationEvent').IAnimationEvent; + type IAnimationHandler = import('./noppes/npcs/api/handler/IAnimationHandler').IAnimationHandler; + type IAnvilRecipe = import('./noppes/npcs/api/handler/data/IAnvilRecipe').IAnvilRecipe; + type IArrow = import('./noppes/npcs/api/entity/IArrow').IArrow; + type IAttributeDefinition = import('./noppes/npcs/api/handler/data/IAttributeDefinition').IAttributeDefinition; + type IAttributeHandler = import('./noppes/npcs/api/handler/IAttributeHandler').IAttributeHandler; + type IAvailability = import('./noppes/npcs/api/handler/data/IAvailability').IAvailability; + type IBlock = import('./noppes/npcs/api/IBlock').IBlock; + type IBlockEvent = import('./noppes/npcs/api/event/IBlockEvent').IBlockEvent; + type IBlockScripted = import('./noppes/npcs/api/block/IBlockScripted').IBlockScripted; + type IBlockState = import('./net/minecraft/block/state/IBlockState').IBlockState; + type IButton = import('./noppes/npcs/api/gui/IButton').IButton; + type ICloneHandler = import('./noppes/npcs/api/handler/ICloneHandler').ICloneHandler; + type ICommand = import('./noppes/npcs/api/ICommand').ICommand; + type IConditionalAction = import('./noppes/npcs/api/handler/data/actions/IConditionalAction').IConditionalAction; + type IContainer = import('./noppes/npcs/api/IContainer').IContainer; + type ICustomAttribute = import('./noppes/npcs/api/handler/data/ICustomAttribute').ICustomAttribute; + type ICustomEffect = import('./noppes/npcs/api/handler/data/ICustomEffect').ICustomEffect; + type ICustomEffectHandler = import('./noppes/npcs/api/handler/ICustomEffectHandler').ICustomEffectHandler; + type ICustomGui = import('./noppes/npcs/api/gui/ICustomGui').ICustomGui; + type ICustomGuiComponent = import('./noppes/npcs/api/gui/ICustomGuiComponent').ICustomGuiComponent; + type ICustomGuiEvent = import('./noppes/npcs/api/event/ICustomGuiEvent').ICustomGuiEvent; + type ICustomNPCsEvent = import('./noppes/npcs/api/event/ICustomNPCsEvent').ICustomNPCsEvent; + type ICustomNpc = import('./noppes/npcs/api/entity/ICustomNpc').ICustomNpc; + type ICustomOverlay = import('./noppes/npcs/api/overlay/ICustomOverlay').ICustomOverlay; + type ICustomOverlayComponent = import('./noppes/npcs/api/overlay/ICustomOverlayComponent').ICustomOverlayComponent; + type IDBCPlayer = import('./noppes/npcs/api/entity/IDBCPlayer').IDBCPlayer; + type IDamageSource = import('./noppes/npcs/api/IDamageSource').IDamageSource; + type IDialog = import('./noppes/npcs/api/handler/data/IDialog').IDialog; + type IDialogCategory = import('./noppes/npcs/api/handler/data/IDialogCategory').IDialogCategory; + type IDialogEvent = import('./noppes/npcs/api/event/IDialogEvent').IDialogEvent; + type IDialogHandler = import('./noppes/npcs/api/handler/IDialogHandler').IDialogHandler; + type IDialogImage = import('./noppes/npcs/api/handler/data/IDialogImage').IDialogImage; + type IDialogOption = import('./noppes/npcs/api/handler/data/IDialogOption').IDialogOption; + type IEntity = import('./noppes/npcs/api/entity/IEntity').IEntity; + type IEntityItem = import('./noppes/npcs/api/entity/IEntityItem').IEntityItem; + type IEntityLiving = import('./noppes/npcs/api/entity/IEntityLiving').IEntityLiving; + type IFaction = import('./noppes/npcs/api/handler/data/IFaction').IFaction; + type IFactionEvent = import('./noppes/npcs/api/event/IFactionEvent').IFactionEvent; + type IFactionHandler = import('./noppes/npcs/api/handler/IFactionHandler').IFactionHandler; + type IFishHook = import('./noppes/npcs/api/entity/IFishHook').IFishHook; + type IForgeEvent = import('./noppes/npcs/api/event/IForgeEvent').IForgeEvent; + type IFrame = import('./noppes/npcs/api/handler/data/IFrame').IFrame; + type IFramePart = import('./noppes/npcs/api/handler/data/IFramePart').IFramePart; + type IItemArmor = import('./noppes/npcs/api/item/IItemArmor').IItemArmor; + type IItemBlock = import('./noppes/npcs/api/item/IItemBlock').IItemBlock; + type IItemBook = import('./noppes/npcs/api/item/IItemBook').IItemBook; + type IItemCustom = import('./noppes/npcs/api/item/IItemCustom').IItemCustom; + type IItemCustomizable = import('./noppes/npcs/api/item/IItemCustomizable').IItemCustomizable; + type IItemEvent = import('./noppes/npcs/api/event/IItemEvent').IItemEvent; + type IItemLinked = import('./noppes/npcs/api/item/IItemLinked').IItemLinked; + type IItemSlot = import('./noppes/npcs/api/gui/IItemSlot').IItemSlot; + type IItemStack = import('./noppes/npcs/api/item/IItemStack').IItemStack; + type IJob = import('./noppes/npcs/api/jobs/IJob').IJob; + type IJobBard = import('./noppes/npcs/api/jobs/IJobBard').IJobBard; + type IJobConversation = import('./noppes/npcs/api/jobs/IJobConversation').IJobConversation; + type IJobFollower = import('./noppes/npcs/api/jobs/IJobFollower').IJobFollower; + type IJobGuard = import('./noppes/npcs/api/jobs/IJobGuard').IJobGuard; + type IJobHealer = import('./noppes/npcs/api/jobs/IJobHealer').IJobHealer; + type IJobItemGiver = import('./noppes/npcs/api/jobs/IJobItemGiver').IJobItemGiver; + type IJobSpawner = import('./noppes/npcs/api/jobs/IJobSpawner').IJobSpawner; + type ILinkedItemEvent = import('./noppes/npcs/api/event/ILinkedItemEvent').ILinkedItemEvent; + type IMarketCategory = import('./noppes/npcs/api/handler/data/IMarketCategory').IMarketCategory; + type IMessage = import('./noppes/npcs/api/handler/data/IMessage').IMessage; + type IModel = import('./noppes/npcs/api/handler/data/IModel').IModel; + type IModelData = import('./noppes/npcs/api/handler/data/IModelData').IModelData; + type IModelPart = import('./noppes/npcs/api/handler/data/IModelPart').IModelPart; + type IModelRotate = import('./noppes/npcs/api/handler/data/IModelRotate').IModelRotate; + type IModelRotatePart = import('./noppes/npcs/api/handler/data/IModelRotatePart').IModelRotatePart; + type IMonster = import('./noppes/npcs/api/entity/IMonster').IMonster; + type INbtTagCompound = import('./noppes/npcs/api/INbt').INbtTagCompound; + type INpcEvent = import('./noppes/npcs/api/event/INpcEvent').INpcEvent; + type IParallel = import('./noppes/npcs/api/handler/data/IParallel').IParallel; + type IParallelLine = import('./noppes/npcs/api/handler/data/IParallelLine').IParallelLine; + type IPartyEvent = import('./noppes/npcs/api/event/IPartyEvent').IPartyEvent; + type IPartyHandler = import('./noppes/npcs/api/handler/IPartyHandler').IPartyHandler; + type IPayloadCompound = import('./noppes/npcs/api/IPayloadCompound').IPayloadCompound; + type IPixelmon = import('./noppes/npcs/api/entity/IPixelmon').IPixelmon; + type IPixelmonPlayerData = import('./noppes/npcs/api/IPixelmonPlayerData').IPixelmonPlayerData; + type IPlayer = import('./noppes/npcs/api/entity/IPlayer').IPlayer; + type IPlayerEvent = import('./noppes/npcs/api/event/IPlayerEvent').IPlayerEvent; + type IPos = import('./noppes/npcs/api/IPos').IPos; + type IProjectile = import('./noppes/npcs/api/entity/IProjectile').IProjectile; + type IProjectileEvent = import('./noppes/npcs/api/event/IProjectileEvent').IProjectileEvent; + type IQuestEvent = import('./noppes/npcs/api/event/IQuestEvent').IQuestEvent; + type IQuestHandler = import('./noppes/npcs/api/handler/IQuestHandler').IQuestHandler; + type IRecipeEvent = import('./noppes/npcs/api/event/IRecipeEvent').IRecipeEvent; + type IRole = import('./noppes/npcs/api/roles/IRole').IRole; + type IRoleArmor = import('./noppes/npcs/api/roles/IRoleArmor').IRoleArmor; + type IRoleDye = import('./noppes/npcs/api/roles/IRoleDye').IRoleDye; + type IRoleHair = import('./noppes/npcs/api/roles/IRoleHair').IRoleHair; + type IRoleHat = import('./noppes/npcs/api/roles/IRoleHat').IRoleHat; + type IRoleHeld = import('./noppes/npcs/api/roles/IRoleHeld').IRoleHeld; + type IRoleLeft = import('./noppes/npcs/api/roles/IRoleLeft').IRoleLeft; + type IRoleRight = import('./noppes/npcs/api/roles/IRoleRight').IRoleRight; + type IRoleSkinPart = import('./noppes/npcs/api/roles/IRoleSkinPart').IRoleSkinPart; + type IRoleSmile = import('./noppes/npcs/api/roles/IRoleSmile').IRoleSmile; + type IScoreboard = import('./noppes/npcs/api/scoreboard/IScoreboard').IScoreboard; + type IScoreboardFont = import('./noppes/npcs/api/scoreboard/IScoreboardFont').IScoreboardFont; + type IScreenSize = import('./noppes/npcs/api/IScreenSize').IScreenSize; + type ISerializable = import('./noppes/npcs/api/ISerializable').ISerializable; + type IShapedRecipe = import('./noppes/npcs/api/IShapedRecipe').IShapedRecipe; + type IShapelessRecipe = import('./noppes/npcs/api/IShapelessRecipe').IShapelessRecipe; + type IShop = import('./noppes/npcs/api/handler/data/IShop').IShop; + type IShopCategory = import('./noppes/npcs/api/handler/data/IShopCategory').IShopCategory; + type IShopItem = import('./noppes/npcs/api/handler/data/IShopItem').IShopItem; + type ISkinOverlay = import('./noppes/npcs/api/ISkinOverlay').ISkinOverlay; + type ISlot = import('./noppes/npcs/api/handler/data/ISlot').ISlot; + type ISound = import('./noppes/npcs/api/handler/data/ISound').ISound; + type ITag = import('./noppes/npcs/api/handler/data/ITag').ITag; + type ITagHandler = import('./noppes/npcs/api/handler/ITagHandler').ITagHandler; + type ITextField = import('./noppes/npcs/api/gui/ITextField').ITextField; + type ITextPlane = import('./noppes/npcs/api/block/ITextPlane').ITextPlane; + type ITexturedRect = import('./noppes/npcs/api/gui/ITexturedRect').ITexturedRect; + type IThrowable = import('./noppes/npcs/api/entity/IThrowable').IThrowable; + type ITileEntity = import('./noppes/npcs/api/ITileEntity').ITileEntity; + type ITimers = import('./noppes/npcs/api/ITimers').ITimers; + type ITransportCategory = import('./noppes/npcs/api/handler/data/ITransportCategory').ITransportCategory; + type ITransportHandler = import('./noppes/npcs/api/handler/ITransportHandler').ITransportHandler; + type ITransportLocation = import('./noppes/npcs/api/handler/data/ITransportLocation').ITransportLocation; + type IVillager = import('./noppes/npcs/api/entity/IVillager').IVillager; + type IWorld = import('./noppes/npcs/api/IWorld').IWorld; + type IntHashMap = import('./net/minecraft/util/math/IntHashMap').IntHashMap; + type Vec3i = import('./net/minecraft/util/Vec3i').Vec3i; + + // ============================================================================ + // GLOBAL API INSTANCE + // ============================================================================ + + const API: import('./noppes/npcs/api/AbstractNpcAPI').AbstractNpcAPI; + + // ============================================================================ + // NESTED INTERFACES - Allow autocomplete like INpcEvent.InitEvent + // ============================================================================ + + namespace INpcEvent { + interface CollideEvent extends INpcEvent {} + interface DamagedEvent extends INpcEvent {} + interface RangedLaunchedEvent extends INpcEvent {} + interface MeleeAttackEvent extends INpcEvent {} + interface SwingEvent extends INpcEvent {} + interface KilledEntityEvent extends INpcEvent {} + interface DiedEvent extends INpcEvent {} + interface InteractEvent extends INpcEvent {} + interface DialogEvent extends INpcEvent {} + interface TimerEvent extends INpcEvent {} + interface TargetEvent extends INpcEvent {} + interface TargetLostEvent extends INpcEvent {} + interface DialogClosedEvent extends INpcEvent {} + interface UpdateEvent extends INpcEvent {} + interface InitEvent extends INpcEvent {} + } + + namespace IPlayerEvent { + interface ChatEvent extends IPlayerEvent {} + interface KeyPressedEvent extends IPlayerEvent {} + interface MouseClickedEvent extends IPlayerEvent {} + interface PickupXPEvent extends IPlayerEvent {} + interface LevelUpEvent extends IPlayerEvent {} + interface LogoutEvent extends IPlayerEvent {} + interface LoginEvent extends IPlayerEvent {} + interface RespawnEvent extends IPlayerEvent {} + interface ChangedDimension extends IPlayerEvent {} + interface TimerEvent extends IPlayerEvent {} + interface AttackedEvent extends IPlayerEvent {} + interface DamagedEvent extends IPlayerEvent {} + interface LightningEvent extends IPlayerEvent {} + interface SoundEvent extends IPlayerEvent {} + interface FallEvent extends IPlayerEvent {} + interface JumpEvent extends IPlayerEvent {} + interface KilledEntityEvent extends IPlayerEvent {} + interface DiedEvent extends IPlayerEvent {} + interface RangedLaunchedEvent extends IPlayerEvent {} + interface AttackEvent extends IPlayerEvent {} + interface DamagedEntityEvent extends IPlayerEvent {} + interface ContainerClosed extends IPlayerEvent {} + interface ContainerOpen extends IPlayerEvent {} + interface PickUpEvent extends IPlayerEvent {} + interface DropEvent extends IPlayerEvent {} + interface TossEvent extends IPlayerEvent {} + interface InteractEvent extends IPlayerEvent {} + interface RightClickEvent extends IPlayerEvent {} + interface UpdateEvent extends IPlayerEvent {} + interface InitEvent extends IPlayerEvent {} + interface StartUsingItem extends IPlayerEvent {} + interface UsingItem extends IPlayerEvent {} + interface StopUsingItem extends IPlayerEvent {} + interface FinishUsingItem extends IPlayerEvent {} + interface BreakEvent extends IPlayerEvent {} + interface UseHoeEvent extends IPlayerEvent {} + interface WakeUpEvent extends IPlayerEvent {} + interface SleepEvent extends IPlayerEvent {} + interface AchievementEvent extends IPlayerEvent {} + interface FillBucketEvent extends IPlayerEvent {} + interface BonemealEvent extends IPlayerEvent {} + interface RangedChargeEvent extends IPlayerEvent {} + interface EffectEvent extends IPlayerEvent {} + interface ProfileEvent extends IPlayerEvent {} + } + + namespace IProjectileEvent { + interface UpdateEvent extends IProjectileEvent {} + interface ImpactEvent extends IProjectileEvent {} + } + + namespace IQuestEvent { + interface QuestStartEvent extends IQuestEvent {} + interface QuestCompletedEvent extends IQuestEvent {} + interface QuestTurnedInEvent extends IQuestEvent {} + } + + namespace IDialogEvent { + interface DialogClosed extends IDialogEvent {} + } + + namespace IFactionEvent { + interface FactionPointsEvent extends IFactionEvent {} + } + + namespace IPartyEvent { + interface PartyQuestCompletedEvent extends IPartyEvent {} + interface PartyQuestSetEvent extends IPartyEvent {} + interface PartyQuestTurnedInEvent extends IPartyEvent {} + interface PartyInviteEvent extends IPartyEvent {} + interface PartyKickEvent extends IPartyEvent {} + interface PartyLeaveEvent extends IPartyEvent {} + interface PartyDisbandEvent extends IPartyEvent {} + } + + namespace IBlockEvent { + interface InitEvent extends IBlockEvent {} + interface InteractEvent extends IBlockEvent {} + interface UpdateEvent extends IBlockEvent {} + interface TimerEvent extends IBlockEvent {} + } + + namespace ICustomGuiEvent { + interface CloseEvent extends ICustomGuiEvent {} + interface ButtonEvent extends ICustomGuiEvent {} + interface SlotEvent extends ICustomGuiEvent {} + interface SlotClickEvent extends ICustomGuiEvent {} + interface ScrollEvent extends ICustomGuiEvent {} + } + + namespace IAnimationEvent { + interface Started extends IAnimationEvent {} + interface Ended extends IAnimationEvent {} + interface IFrameEvent extends IAnimationEvent {} + } + + namespace IItemEvent { + interface InitEvent extends IItemEvent {} + interface UpdateEvent extends IItemEvent {} + interface TossedEvent extends IItemEvent {} + interface PickedUpEvent extends IItemEvent {} + interface SpawnEvent extends IItemEvent {} + interface InteractEvent extends IItemEvent {} + interface RightClickEvent extends IItemEvent {} + interface AttackEvent extends IItemEvent {} + interface StartUsingItem extends IItemEvent {} + interface UsingItem extends IItemEvent {} + interface StopUsingItem extends IItemEvent {} + interface FinishUsingItem extends IItemEvent {} + } + + // ============================================================================ + // AMBIENT HANDLER FUNCTIONS - Defined in hooks.d.ts + // ============================================================================ + // For type-safe handlers with overloads (parameter names determine event type), + // see hooks.d.ts which includes declare function overloads. + // + // Example: + // function damaged(INpcEvent: INpcEvent.DamagedEvent) { INpcEvent. } // infers INpcEvent.DamagedEvent + // function damaged(IPlayerEvent: IPlayerEvent.DamagedEvent) { IPlayerEvent. } // infers IPlayerEvent.DamagedEvent +} + +export {}; diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts new file mode 100644 index 000000000..58f98ec62 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts @@ -0,0 +1,162 @@ +/** + * Generated from Java file for CustomNPC+ DBC Addon + * Package: kamkeel.npcdbc.api + */ + +import { IDBCPlayer } from '../../../noppes/npcs/api/entity/IDBCPlayer'; +import { IEntityLivingBase } from '../../../noppes/npcs/api/entity/IEntityLivingBase'; +import { IForm } from './form/IForm'; +import { IAura } from './aura/IAura'; +import { IOutline } from './outline/IOutline'; +import { IKiAttack } from './IKiAttack'; +import { IPlayer } from '../../../noppes/npcs/api/entity/IPlayer'; + +export interface IDBCAddon extends IDBCPlayer { + getAllFullAttributes(): number[]; + getUsedMind(): number; + getAvailableMind(): number; + setLockOnTarget(lockOnTarget: IEntityLivingBase): void; + setKiFistOn(on: boolean): void; + setKiProtectionOn(on: boolean): void; + setKiWeaponType(type: number): void; + kiFistOn(): boolean; + kiProtectionOn(): boolean; + getKiWeaponType(): number; + isTurboOn(): boolean; + setTurboState(on: boolean): void; + getMaxBody(): number; + getMaxHP(): number; + getBodyPercentage(): number; + getMaxKi(): number; + getMaxStamina(): number; + getAllAttributes(): number[]; + modifyAllAttributes(attri: number[], multiplyAddedAttris: boolean, multiValue: number): void; + modifyAllAttributes(Num: number, setStatsToNum: boolean): void; + modifyAllAttributes(submitted: number[], setStats: boolean): void; + multiplyAttribute(statid: number, multi: number): void; + multiplyAllAttributes(multi: number): void; + getFullAttribute(statid: number): number; + getRaceName(): string; + getCurrentDBCFormName(): string; + changeDBCMastery(formName: string, amount: number, add: boolean): void; + getDBCMasteryValue(formName: string): number; + getAllDBCMasteries(): string; + isDBCFusionSpectator(): boolean; + isChargingKi(): boolean; + getSkillLevel(skillname: string): number; + getMaxStat(attribute: number): number; + getCurrentStat(attribute: number): number; + getCurrentFormMultiplier(): number; + getMajinAbsorptionRace(): number; + setMajinAbsorptionRace(race: number): void; + getMajinAbsorptionPower(): number; + setMajinAbsorptionPower(power: number): void; + isMUI(): boolean; + isKO(): boolean; + isUI(): boolean; + isMystic(): boolean; + isKaioken(): boolean; + isGOD(): boolean; + isLegendary(): boolean; + isDivine(): boolean; + isMajin(): boolean; + + // Custom Form Setters (Grouped) + setCustomForm(formID: number): void; + setCustomForm(formID: number, ignoreUnlockCheck: boolean): void; + setCustomForm(form: IForm): void; + setCustomForm(form: IForm, ignoreUnlockCheck: boolean): void; + setCustomForm(formName: string): void; + setCustomForm(formName: string, ignoreUnlockCheck: boolean): void; + + // Custom Form Management (Grouped) + hasCustomForm(formName: string): boolean; + hasCustomForm(formID: number): boolean; + getCustomForms(): IForm[]; + giveCustomForm(formName: string): void; + giveCustomForm(form: IForm): void; + removeCustomForm(formName: string): void; + removeCustomForm(formName: string, removesMastery: boolean): void; + removeCustomForm(form: IForm): void; + removeCustomForm(form: IForm, removesMastery: boolean): void; + + // Flight + setFlight(flightOn: boolean): void; + isFlying(): boolean; + setAllowFlight(allowFlight: boolean): void; + setFlightSpeedRelease(release: number): void; + setBaseFlightSpeed(speed: number): void; + setDynamicFlightSpeed(speed: number): void; + setFlightGravity(isEffected: boolean): void; + setFlightDefaults(): void; + setSprintSpeed(speed: number): void; + + // Selected Form + getSelectedForm(): IForm; + setSelectedForm(form: IForm): void; + setSelectedForm(formID: number): void; + removeSelectedForm(): void; + getSelectedDBCForm(): number; + setSelectedDBCForm(formID: number): void; + removeSelectedDBCForm(): void; + getCurrentForm(): IForm; + isInCustomForm(): boolean; + isInCustomForm(form: IForm): boolean; + isInCustomForm(formID: number): boolean; + + // Aura + setAuraSelection(auraName: string): void; + setAuraSelection(aura: IAura): void; + setAuraSelection(auraID: number): void; + giveAura(auraName: string): void; + giveAura(aura: IAura): void; + giveAura(auraID: number): void; + hasAura(auraName: string): boolean; + hasAura(auraId: number): boolean; + removeAura(auraName: string): void; + removeAura(aura: IAura): void; + removeAura(auraID: number): void; + setAura(auraName: string): void; + setAura(aura: IAura): void; + setAura(auraID: number): void; + removeCurrentAura(): void; + removeAuraSelection(): void; + isInAura(): boolean; + isInAura(aura: IAura): boolean; + isInAura(auraName: string): boolean; + isInAura(auraID: number): boolean; + + // Mastery + setCustomMastery(formID: number, value: number): void; + setCustomMastery(formID: number, value: number, ignoreUnlockCheck: boolean): void; + setCustomMastery(form: IForm, value: number): void; + setCustomMastery(form: IForm, value: number, ignoreUnlockCheck: boolean): void; + addCustomMastery(formID: number, value: number): void; + addCustomMastery(formID: number, value: number, ignoreUnlockCheck: boolean): void; + addCustomMastery(form: IForm, value: number): void; + addCustomMastery(form: IForm, value: number, ignoreUnlockCheck: boolean): void; + getCustomMastery(formID: number): number; + getCustomMastery(formID: number, checkFusion: boolean): number; + getCustomMastery(form: IForm): number; + getCustomMastery(form: IForm, checkFusion: boolean): number; + removeCustomMastery(formID: number): void; + removeCustomMastery(form: IForm): void; + + // Outline + removeOutline(): void; + setOutline(outline: IOutline): void; + setOutline(outlineName: string): void; + setOutline(outlineID: number): void; + getOutline(): IOutline; + + // Other + getFusionPartner(): IPlayer; + fireKiAttack(type: number, speed: number, damage: number, hasEffect: boolean, color: number, density: number, hasSound: boolean, chargePercent: number): void; + fireKiAttack(kiAttack: IKiAttack): void; + isReleasing(): boolean; + isMeditating(): boolean; + isSuperRegen(): boolean; + isSwooping(): boolean; + isInMedicalLiquid(): boolean; + getAttackFromSlot(slot: number): IKiAttack; +} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts new file mode 100644 index 000000000..1e7503cb5 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts @@ -0,0 +1,22 @@ +export interface IKiAttack { + getType(): number; + setType(type: number): void; + getSpeed(): number; + setSpeed(speed: number): void; + getDamage(): number; + setDamage(damage: number): void; + hasEffect(): boolean; + setHasEffect(hasEffect: boolean): void; + getColor(): number; + setColor(color: number): void; + getDensity(): number; + setDensity(density: number): void; + hasSound(): boolean; + setHasSound(hasSound: boolean): void; + getChargePercent(): number; + setChargePercent(chargePercent: number): void; + respectFormDestoryerConfig(): boolean; + setRespectFormDestroyerConfig(respectFormConfig: boolean): void; + isDestroyerAttack(): boolean; + setDestroyerAttack(isDestroyer: boolean): void; +} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts new file mode 100644 index 000000000..66968ff0b --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts @@ -0,0 +1,19 @@ +export interface IAura { + getName(): string; + setName(name: string): void; + getMenuName(): string; + setMenuName(name: string): void; + getID(): number; + setID(newID: number): void; + assignToPlayer(player: IPlayer): void; + removeFromPlayer(player: IPlayer): void; + assignToPlayer(playerName: string): void; + removeFromPlayer(playerName: string): void; + getDisplay(): any; // IAuraDisplay + getSecondaryAuraID(): number; + getSecondaryAura(): IAura; + setSecondaryAura(id: number): void; + setSecondaryAura(aura: IAura): void; + clone(): IAura; + save(): IAura; +} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts new file mode 100644 index 000000000..02347f3eb --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts @@ -0,0 +1,49 @@ +/** + * Generated from Java file for CustomNPC+ DBC Addon + * Package: kamkeel.npcdbc.api.event + */ + +import { IPlayerEvent } from '../../../noppes/npcs/api/event/IPlayerEvent'; +import { IDamageSource } from '../../../noppes/npcs/api/IDamageSource'; + +export type IDBCEvent = IPlayerEvent + +export namespace IDBCEvent { + export interface CapsuleUsedEvent extends IDBCEvent { + getType(): number; + getSubType(): number; + getCapsuleName(): string; + } + + export type SenzuUsedEvent = IDBCEvent + + export interface FormChangeEvent extends IDBCEvent { + getFormBeforeID(): number; + getFormAfterID(): number; + isFormBeforeCustom(): boolean; + isFormAfterCustom(): boolean; + } + + export interface DamagedEvent extends IDBCEvent { + getDamage(): number; + setDamage(damage: number): void; + + getStaminaReduced(): number; + setStaminaReduced(stamina: number): void; + + willKo(): boolean; + + getKiReduced(): number; + setKiReduced(ki: number): void; + + getDamageSource(): IDamageSource; + isDamageSourceKiAttack(): boolean; + getType(): number; + } + + export type DBCReviveEvent = IDBCEvent + + export interface DBCKnockout extends IDBCEvent { + getDamageSource(): IDamageSource; + } +} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts new file mode 100644 index 000000000..e7e5f5926 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts @@ -0,0 +1,55 @@ +export interface IForm { + getName(): string; + setName(name: string): void; + getMenuName(): string; + setMenuName(name: string): void; + getRace(): number; + setRace(race: number): void; + getAllMulti(): number[]; // float[] + setAllMulti(allMulti: number): void; // float + raceEligible(race: number): boolean; + raceEligible(player: IPlayer): boolean; + setAttributeMulti(id: number, multi: number): void; + getAttributeMulti(id: number): number; + assignToPlayer(player: IPlayer): void; + removeFromPlayer(player: IPlayer): void; + assignToPlayer(playerName: string): void; + removeFromPlayer(playerName: string): void; + removeFromPlayer(player: IPlayer, removesMastery: boolean): void; + removeFromPlayer(playerName: string, removesMastery: boolean): void; + getAscendSound(): string; + setAscendSound(directory: string): void; + getDescendSound(): string; + setDescendSound(directory: string): void; + getID(): number; + setID(newID: number): void; + getChildID(): number; + hasChild(): boolean; + linkChild(formID: number): void; + linkChild(form: IForm): void; + isFromParentOnly(): boolean; + setFromParentOnly(set: boolean): void; + addFormRequirement(race: number, state: number): void; + removeFormRequirement(race: number): void; + getFormRequirement(race: number): number; + isChildOf(parent: IForm): boolean; + getChild(): IForm; + removeChildForm(): void; + getParentID(): number; + hasParent(): boolean; + linkParent(formID: number): void; + linkParent(form: IForm): void; + getParent(): IForm; + getTimer(): number; + setTimer(timeInTicks: number): void; + hasTimer(): boolean; + removeParentForm(): void; + getMastery(): any; // IFormMastery + getDisplay(): any; // IFormDisplay + getStackable(): any; // IFormStackable + getAdvanced(): any; // IFormAdvanced + setMindRequirement(mind: number): void; + getMindRequirement(): number; + clone(): IForm; + save(): IForm; +} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts new file mode 100644 index 000000000..5e6050320 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts @@ -0,0 +1,18 @@ +export interface IOutline { + setInnerColor(color: number, alpha: number): void; + setOuterColor(color: number, alpha: number): void; + setSize(size: number): IOutline; + setNoiseSize(size: number): IOutline; + setSpeed(speed: number): IOutline; + setPulsingSpeed(speed: number): IOutline; + setColorSmoothness(smoothness: number): IOutline; + setColorInterpolation(interp: number): IOutline; + getName(): string; + setName(name: string): void; + getMenuName(): string; + setMenuName(name: string): void; + getID(): number; + setID(newID: number): void; + clone(): IOutline; + save(): IOutline; +} diff --git a/src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts b/src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts new file mode 100644 index 000000000..a0695de2f --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts @@ -0,0 +1,278 @@ +/** + * Raw Minecraft 1.7.10 Type Definitions (MCP Names) + * These represent the internal deobfuscated Minecraft classes. + */ + +declare global { + namespace net.minecraft { + export namespace block { + export interface Block { + /** @returns The unlocalized name of the block */ + getUnlocalizedName(): string; + /** @returns The localized name of the block */ + getLocalizedName(): string; + /** @returns Light opacity of the block */ + getLightOpacity(): number; + /** @returns Light value of the block */ + getLightValue(): number; + } + } + + export namespace damage { + export interface DamageSource { + /** The type of damage (e.g. "player", "fall", "magic") */ + damageType: string; + /** @returns True if damage bypasses armor */ + isUnblockable(): boolean; + /** @returns True if damage is absolute (bypasses potions etc) */ + isDamageAbsolute(): boolean; + /** @returns The entity that caused the damage */ + getEntity(): net.minecraft.entity.Entity; + /** @returns The entity that directly dealt the damage (e.g. arrow) */ + getSourceOfDamage(): net.minecraft.entity.Entity; + } + } + + export namespace entity { + /** Base class for all entities in Minecraft 1.7.10 */ + export interface Entity { + /** The entity's unique ID */ + entityId: number; + /** The world the entity is in */ + worldObj: net.minecraft.world.World; + /** X position */ + posX: number; + /** Y position */ + posY: number; + /** Z position */ + posZ: number; + /** X motion */ + motionX: number; + /** Y motion */ + motionY: number; + /** Z motion */ + motionZ: number; + /** Rotation yaw */ + rotationYaw: number; + /** Rotation pitch */ + rotationPitch: number; + /** Entity width */ + width: number; + /** Entity height */ + height: number; + /** Whether the entity is on the ground */ + onGround: boolean; + /** Whether the entity is in water */ + isInWeb: boolean; + /** Ticks the entity has been alive */ + ticksExisted: number; + /** Remaining air */ + air: number; + /** Damage sustained from falling */ + fallDistance: number; + + /** Gets a unique string key for this entity type */ + getEntityString(): string; + /** Sets the entity's position */ + setPosition(x: number, y: number, z: number): void; + /** Sets the entity's rotation */ + setRotation(yaw: number, pitch: number): void; + /** Marks the entity for removal */ + setDead(): void; + /** @returns True if the entity is burning */ + isBurning(): boolean; + /** @returns True if the entity is sneaking */ + isSneaking(): boolean; + /** @returns True if the entity is sprinting */ + isSprinting(): boolean; + /** @returns True if the entity is invisible */ + isInvisible(): boolean; + } + + export interface EntityLivingBase extends Entity { + /** @returns Current health */ + getHealth(): number; + /** Sets the entity's health */ + setHealth(health: number): void; + /** @returns Max health */ + getMaxHealth(): number; + /** @returns True if entity is child */ + isChild(): boolean; + /** Sets entity on fire */ + setFire(seconds: number): void; + } + + export interface EntityLiving extends EntityLivingBase { + /** Sets the attack target */ + setAttackTarget(target: net.minecraft.entity.EntityLivingBase): void; + /** @returns The current attack target */ + getAttackTarget(): net.minecraft.entity.EntityLivingBase; + /** @returns True if the entity can pick up loot */ + canPickUpLoot(): boolean; + /** Sets if the entity can pick up loot */ + setCanPickUpLoot(can: boolean): void; + } + + export interface EntityItem extends Entity { + /** The ItemStack contained in this entity */ + getEntityItem(): net.minecraft.item.ItemStack; + /** Sets the ItemStack contained in this entity */ + setEntityItemStack(stack: net.minecraft.item.ItemStack): void; + } + + export interface EntityZombie extends EntityLiving { + /** @returns True if the zombie is a child */ + isChild(): boolean; + /** Sets if the zombie is a child */ + setChild(child: boolean): void; + } + + export namespace player { + /** Representation of a Player in Minecraft 1.7.10 */ + export interface EntityPlayer extends net.minecraft.entity.Entity { + /** The player's inventory */ + inventory: net.minecraft.entity.player.InventoryPlayer; + /** The player's experience level */ + experienceLevel: number; + /** Total experience points */ + experienceTotal: number; + /** Progress to next level (0.0 to 1.0) */ + experience: number; + /** Player capabilities (flying, creative, etc.) */ + capabilities: net.minecraft.entity.player.PlayerCapabilities; + /** Food stats (hunger, saturation) */ + foodStats: any; + /** The player's dimension ID */ + dimension: number; + + /** Sends a chat message to the player */ + addChatMessage(component: any): void; + /** @returns The display name of the player */ + getDisplayName(): string; + /** @returns The actual username/command sender name */ + getCommandSenderName(): string; + /** Closes any open GUI */ + closeScreen(): void; + } + + export interface InventoryPlayer { + /** Current selected slot index (0-8) */ + currentItem: number; + /** Main inventory array (36 slots) */ + mainInventory: net.minecraft.item.ItemStack[]; + /** Armor inventory array (4 slots) */ + armorInventory: net.minecraft.item.ItemStack[]; + + /** Gets the currently held item */ + getCurrentItem(): net.minecraft.item.ItemStack; + /** Consumes 1 of the item in the specified slot */ + consumeInventoryItem(item: any): boolean; + /** Checks if the player has a specific item */ + hasItem(item: any): boolean; + } + + export interface PlayerCapabilities { + /** Whether the player is invulnerable */ + disableDamage: boolean; + /** Whether the player is flying */ + isFlying: boolean; + /** Whether the player can fly */ + allowFlying: boolean; + /** Whether the player is in creative mode */ + isCreativeMode: boolean; + /** Flying speed */ + flySpeed: number; + /** Walking speed */ + walkSpeed: number; + } + } + } + + export namespace world { + /** Representation of the World in Minecraft 1.7.10 */ + export interface World { + /** World provider (dimension info) */ + provider: net.minecraft.world.WorldProvider; + /** List of all entities in the world */ + loadedEntityList: net.minecraft.entity.Entity[]; + /** List of all players in the world */ + playerEntities: net.minecraft.entity.player.EntityPlayer[]; + /** True if this is a client-side world */ + isRemote: boolean; + /** World time in ticks */ + worldTime: number; + /** The world's difficulty (0-3) */ + difficultySetting: number; + + /** @returns The block at the given coordinates */ + getBlock(x: number, y: number, z: number): net.minecraft.block.Block; + /** @returns The metadata of the block at the given coordinates */ + getBlockMetadata(x: number, y: number, z: number): number; + /** Sets the block and metadata at the given coordinates */ + setBlock(x: number, y: number, z: number, block: net.minecraft.block.Block, metadata: number, flags: number): boolean; + /** Spawns an entity in the world */ + spawnEntityInWorld(entity: net.minecraft.entity.Entity): boolean; + /** Plays a sound at the given coordinates */ + playSoundEffect(x: number, y: number, z: number, name: string, volume: number, pitch: number): void; + } + + export interface WorldProvider { + /** Dimension ID (0 = Overworld, -1 = Nether, 1 = End) */ + dimensionId: number; + /** Name of the dimension type */ + dimensionName: string; + /** Whether this dimension has a sky */ + hasSkyLight: boolean; + } + } + + export namespace item { + /** Representation of an Item stack in Minecraft 1.7.10 */ + export interface ItemStack { + /** Number of items in the stack */ + stackSize: number; + /** Damage/Metadata value of the item */ + itemDamage: number; + /** The underlying Item object */ + item: any; + /** NBT data of the stack */ + stackTagCompound: any; + + /** @returns The display name of the item */ + getDisplayName(): string; + /** @returns True if the item has NBT data */ + hasTagCompound(): boolean; + /** Copies the item stack */ + copy(): net.minecraft.item.ItemStack; + } + } + + export namespace util { + /** Result of a raytrace in 1.7.10 */ + export interface MovingObjectPosition { + /** 0 = MISS, 1 = BLOCK, 2 = ENTITY */ + typeOfHit: number; + /** X coordinate of the hit block */ + blockX: number; + /** Y coordinate of the hit block */ + blockY: number; + /** Z coordinate of the hit block */ + blockZ: number; + /** Side of the block hit (0-5) */ + sideHit: number; + /** The entity hit, if typeOfHit is ENTITY */ + entityHit: net.minecraft.entity.Entity; + /** The exact hit vector */ + hitVec: net.minecraft.util.Vec3; + } + + export interface Vec3 { + xCoord: number; + yCoord: number; + zCoord: number; + } + } + } +} + +export { }; diff --git a/src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts b/src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts new file mode 100644 index 000000000..a40d27121 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts @@ -0,0 +1,18 @@ +/** + * Generated from Java interface net.minecraft.block.properties.IProperty + * Source: CustomNPC+ JavaDoc (Minecraft 1.7.10) + */ + +export interface IProperty { + /** Returns the property name */ + getName(): string; + + /** Returns the allowed values for this property */ + getAllowedValues(): Array; + + /** Returns the runtime value class for this property (approximation) */ + getValueClass(): Function; + + /** Returns the name for a given value */ + getName(value: T): string; +} diff --git a/src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts b/src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts new file mode 100644 index 000000000..7361d198a --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts @@ -0,0 +1,16 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: net.minecraft.block.state + */ + +export interface IBlockState { + + // Methods + getPropertyNames(): Array { + + // Fields + x: number; + y: number; + z: number; + xd: number; + yd: number; + zd: number; + true: any; + false: any; + vec3I: import('./Vec3i').Vec3i; + + // Methods + equals(p_equals_1_: any): boolean; + hashCode(): number; + compareTo(p_compareTo_1_: import('./Vec3i').Vec3i): number; + getX(): number; + getY(): number; + getZ(): number; + getXD(): number; + getYD(): number; + getZD(): number; + crossProduct(vec: import('./Vec3i').Vec3i): i; + toString(): string; + +} diff --git a/src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts b/src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts new file mode 100644 index 000000000..df828d373 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts @@ -0,0 +1,82 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: net.minecraft.util.math + */ + +export class BlockPos extends Vec3i { + + // Fields + LOGGER: Logger; + ORIGIN: import('./BlockPos').BlockPos; + NUM_X_BITS: number; + NUM_Z_BITS: number; + NUM_Y_BITS: number; + Y_SHIFT: number; + X_SHIFT: number; + X_MASK: number; + Y_MASK: number; + Z_MASK: number; + x: any; + x: any; + n: any; + 0: <<; + i: number; + j: number; + k: number; + blockpos: import('./BlockPos').BlockPos; + blockpos1: import('./BlockPos').BlockPos; + lastReturned: import('./BlockPos').BlockPos; + i: number; + j: number; + k: number; + x: number; + y: number; + z: number; + this: any; + released: boolean; + POOL: Array { + + // Methods + isBreedingItem(itemStack: import('../item/IItemStack').IItemStack): boolean; + interact(player: import('./IPlayer').IPlayer): boolean; + setFollowPlayer(player: import('./IPlayer').IPlayer): void; + followingPlayer(): import('./IPlayer').IPlayer; + isInLove(): boolean; + resetInLove(): void; + canMateWith(animal: import('./IAnimal').IAnimal): boolean; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts new file mode 100644 index 000000000..bd933971b --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts @@ -0,0 +1,11 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IAnimatable { + + // Methods + getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts new file mode 100644 index 000000000..719076b1a --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts @@ -0,0 +1,13 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IArrow extends IEntity { + + // Methods + getShooter(): import('./IEntity').IEntity; + getDamage(): number; + kill(): void; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts new file mode 100644 index 000000000..d99948448 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts @@ -0,0 +1,217 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface ICustomNpc extends IEntityLiving, IAnimatable { + + // Methods + getSize(): number; + setSize(size: number): void; + getModelType(): number; + setModelType(modelType: number): void; + getName(): string; + setRotation(rotation: number): void; + setRotationType(rotationType: number): void; + getRotationType(): number; + setMovingType(movingType: number): void; + getMovingType(): number; + setName(name: string): void; + getTitle(): string; + setTitle(title: string): void; + getTexture(): string; + setTexture(texture: string): void; + getHome(): import('../IPos').IPos; + getHomeX(): number; + setHomeX(x: number): void; + getHomeY(): number; + setHomeY(y: number): void; + getHomeZ(): number; + setHomeZ(z: number): void; + setHome(x: number, y: number, z: number): void; + setHome(pos: import('../IPos').IPos): void; + setMaxHealth(health: number): void; + setReturnToHome(bo: boolean): void; + getReturnToHome(): boolean; + getFaction(): import('../handler/data/IFaction').IFaction; + setFaction(id: number): void; + setAttackFactions(attackOtherFactions: boolean): void; + getAttackFactions(): boolean; + setDefendFaction(defendFaction: boolean): void; + getDefendFaction(): boolean; + getType(): number; + typeOf(type: number): boolean; + shootItem(target: IEntityLivingBase, item: import('../item/IItemStack').IItemStack, accuracy: number): void; + setProjectilesKeepTerrain(b: boolean): void; + getProjectilesKeepTerrain(): boolean; + say(message: string): void; + say(player: import('./IPlayer').IPlayer, message: string): void; + getDialog(slot: number): import('../handler/data/IDialog').IDialog; + getDialogId(slot: number): number; + setDialog(slot: number, dialog: import('../handler/data/IDialog').IDialog): void; + setDialog(slot: number, dialogId: number): void; + getInteractLines(): import('../handler/data/ILines').ILines; + getWorldLines(): import('../handler/data/ILines').ILines; + getAttackLines(): import('../handler/data/ILines').ILines; + getKilledLines(): import('../handler/data/ILines').ILines; + getKillLines(): import('../handler/data/ILines').ILines; + kill(): void; + reset(): void; + getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; + getRole(): import('../roles/IRole').IRole; + setRole(role: number): void; + getJob(): import('../jobs/IJob').IJob; + setJob(job: number): void; + getRightItem(): import('../item/IItemStack').IItemStack; + setRightItem(item: import('../item/IItemStack').IItemStack): void; + getLefttItem(): import('../item/IItemStack').IItemStack; + getLeftItem(): import('../item/IItemStack').IItemStack; + setLeftItem(item: import('../item/IItemStack').IItemStack): void; + getProjectileItem(): import('../item/IItemStack').IItemStack; + setProjectileItem(item: import('../item/IItemStack').IItemStack): void; + canAimWhileShooting(): boolean; + aimWhileShooting(aimWhileShooting: boolean): void; + setAimType(aimWhileShooting: byte): void; + getAimType(): byte; + setMinProjectileDelay(minDelay: number): void; + getMinProjectileDelay(): number; + setMaxProjectileDelay(maxDelay: number): void; + getMaxProjectileDelay(): number; + setRangedRange(rangedRange: number): void; + getRangedRange(): number; + setFireRate(rate: number): void; + getFireRate(): number; + setBurstCount(burstCount: number): void; + getBurstCount(): number; + setShotCount(shotCount: number): void; + getShotCount(): number; + setAccuracy(accuracy: number): void; + getAccuracy(): number; + getFireSound(): string; + setFireSound(fireSound: string): void; + getArmor(slot: number): import('../item/IItemStack').IItemStack; + setArmor(slot: number, item: import('../item/IItemStack').IItemStack): void; + getLootItem(slot: number): import('../item/IItemStack').IItemStack; + setLootItem(slot: number, item: import('../item/IItemStack').IItemStack): void; + getLootChance(slot: number): number; + setLootChance(slot: number, chance: number): void; + getLootMode(): number; + setLootMode(lootMode: number): void; + setMinLootXP(lootXP: number): void; + setMaxLootXP(lootXP: number): void; + getMinLootXP(): number; + getMaxLootXP(): number; + getCanDrown(): boolean; + setDrowningType(type: number): void; + canBreathe(): boolean; + setAnimation(type: number): void; + setTacticalVariant(variant: number): void; + getTacticalVariant(): number; + setTacticalVariant(variant: string): void; + getTacticalVariantName(): string; + getCombatPolicyName(): string; + setCombatPolicy(policy: number): void; + getCombatPolicy(): number; + setCombatPolicy(policy: string): void; + setTacticalRadius(tacticalRadius: number): void; + getTacticalRadius(): number; + setIgnoreCobweb(ignore: boolean): void; + getIgnoreCobweb(): boolean; + setOnFoundEnemy(onAttack: number): void; + onFoundEnemy(): number; + setShelterFrom(shelterFrom: number): void; + getShelterFrom(): number; + hasLivingAnimation(): boolean; + setLivingAnimation(livingAnimation: boolean): void; + setVisibleType(type: number): void; + getVisibleType(): number; + setVisibleTo(player: import('./IPlayer').IPlayer, visible: boolean): void; + isVisibleTo(player: import('./IPlayer').IPlayer): boolean; + setShowName(type: number): void; + getShowName(): number; + getShowBossBar(): number; + setShowBossBar(type: number): void; + getMeleeStrength(): number; + setMeleeStrength(strength: number): void; + getMeleeSpeed(): number; + setMeleeSpeed(speed: number): void; + getMeleeRange(): number; + setMeleeRange(range: number): void; + getSwingWarmup(): number; + setSwingWarmup(ticks: number): void; + getKnockback(): number; + setKnockback(knockback: number): void; + getAggroRange(): number; + setAggroRange(aggroRange: number): void; + getRangedStrength(): number; + setRangedStrength(strength: number): void; + getRangedSpeed(): number; + setRangedSpeed(speed: number): void; + getRangedBurst(): number; + setRangedBurst(count: number): void; + getRespawnTime(): number; + setRespawnTime(time: number): void; + getRespawnCycle(): number; + setRespawnCycle(cycle: number): void; + getHideKilledBody(): boolean; + hideKilledBody(hide: boolean): void; + naturallyDespawns(): boolean; + setNaturallyDespawns(canDespawn: boolean): void; + spawnedFromSoulStone(): boolean; + getSoulStonePlayerName(): string; + isSoulStoneInit(): boolean; + getRefuseSoulStone(): boolean; + setRefuseSoulStone(refuse: boolean): void; + getMinPointsToSoulStone(): number; + setMinPointsToSoulStone(points: number): void; + giveItem(player: import('./IPlayer').IPlayer, item: import('../item/IItemStack').IItemStack): void; + executeCommand(command: string): void; + getModelData(): import('./data/IModelData').IModelData; + setHeadScale(x: number, y: number, z: number): void; + setBodyScale(x: number, y: number, z: number): void; + setArmsScale(x: number, y: number, z: number): void; + setLegsScale(x: number, y: number, z: number): void; + setExplosionResistance(resistance: number): void; + getExplosionResistance(): number; + setMeleeResistance(resistance: number): void; + getMeleeResistance(): number; + setArrowResistance(resistance: number): void; + getArrowResistance(): number; + setKnockbackResistance(resistance: number): void; + getKnockbackResistance(): number; + setRetaliateType(type: number): void; + getCombatRegen(): number; + setCombatRegen(regen: number): void; + getHealthRegen(): number; + setHealthRegen(regen: number): void; + getAge(): number; + getTimers(): import('../ITimers').ITimers; + setFly(fly: number): void; + canFly(): boolean; + setFlySpeed(flySpeed: number): void; + getFlySpeed(unused: number): number; + setFlyGravity(flyGravity: number): void; + getFlyGravity(unused: number): number; + setFlyHeightLimit(flyHeightLimit: number): void; + getFlyHeightLimit(unused: number): number; + limitFlyHeight(limit: boolean): void; + isFlyHeightLimited(unused: boolean): boolean; + setSpeed(speed: number): void; + getSpeed(): number; + setSkinType(type: byte): void; + getSkinType(): byte; + setSkinUrl(url: string): void; + getSkinUrl(): string; + setCloakTexture(cloakTexture: string): void; + getCloakTexture(): string; + setOverlayTexture(overlayTexture: string): void; + getOverlayTexture(): string; + getOverlays(): import('../handler/IOverlayHandler').IOverlayHandler; + setCollisionType(type: number): void; + getCollisionType(): number; + updateClient(): void; + updateAI(): void; + getActionManager(): import('../handler/IActionManager').IActionManager; + getMagicData(): import('../handler/data/IMagicData').IMagicData; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts new file mode 100644 index 000000000..0d3fa22c9 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts @@ -0,0 +1,71 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IDBCPlayer extends IPlayer { + + // Methods + setStat(stat: string, value: number): void; + getStat(stat: string): number; + addBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number): void; + addBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number, endOfTheList: boolean): void; + addToBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number): void; + setBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number): void; + getBonusAttribute(stat: string, bonusID: string): void; + removeBonusAttribute(stat: string, bonusID: string): void; + clearBonusAttribute(stat: string): void; + bonusAttribute(action: string, stat: string, bonusID: string): string; + bonusAttribute(action: string, stat: string, bonusID: string, operation: string, attributeValue: number, endOfTheList: boolean): string; + setRelease(release: byte): void; + getRelease(): byte; + setBody(body: number): void; + getBody(): number; + setHP(hp: number): void; + getHP(): number; + setStamina(stamina: number): void; + getStamina(): number; + setKi(ki: number): void; + getKi(): number; + setTP(tp: number): void; + getTP(): number; + setGravity(gravity: number): void; + getGravity(): number; + isBlocking(): boolean; + setHairCode(hairCode: string): void; + getHairCode(): string; + setExtraCode(extraCode: string): void; + getExtraCode(): string; + setItem(itemStack: import('../item/IItemStack').IItemStack, slot: byte, vanity: boolean): void; + getItem(slot: byte, vanity: boolean): import('../item/IItemStack').IItemStack; + getInventory(): import('../item/IItemStack').IItemStack[]; + setForm(form: byte): void; + getForm(): byte; + setForm2(form2: byte): void; + getForm2(): byte; + getRacialFormMastery(form: byte): number; + setRacialFormMastery(form: byte, value: number): void; + addRacialFormMastery(form: byte, value: number): void; + getOtherFormMastery(formName: string): number; + setOtherFormMastery(formName: string, value: number): void; + addOtherFormMastery(formName: string, value: number): void; + setPowerPoints(points: number): void; + getPowerPoints(): number; + setAuraColor(color: number): void; + getAuraColor(): number; + setFormLevel(level: number): void; + getFormLevel(): number; + setSkills(skills: string): void; + getSkills(): string; + setJRMCSE(statusEffects: string): void; + getJRMCSE(): string; + setRace(race: byte): void; + getRace(): number; + setDBCClass(dbcClass: byte): void; + getDBCClass(): byte; + setPowerType(powerType: byte): void; + getPowerType(): number; + getKillCount(type: string): number; + getFusionString(): string; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts new file mode 100644 index 000000000..49f0fd439 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts @@ -0,0 +1,101 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IEntity { + + // Methods + spawnParticle(entityParticle: import('../IParticle').IParticle): void; + getEntityId(): number; + getUniqueID(): string; + getYOffset(): number; + getWidth(): number; + getHeight(): number; + getX(): number; + setX(x: number): void; + getY(): number; + setY(y: number): void; + getZ(): number; + setZ(z: number): void; + getMotionX(): number; + setMotionX(x: number): void; + getMotionY(): number; + setMotionY(y: number): void; + getMotionZ(): number; + setMotionZ(z: number): void; + setMotion(x: number, y: number, z: number): void; + setMotion(pos: import('../IPos').IPos): void; + getMotion(): import('../IPos').IPos; + isAirborne(): boolean; + getBlockX(): number; + getBlockY(): number; + getBlockZ(): number; + setPosition(x: number, y: number, z: number): void; + setPosition(pos: import('../IPos').IPos): void; + getPosition(): import('../IPos').IPos; + getDimension(): number; + setDimension(dimensionId: number): void; + getCollidingEntities(): import('./IEntity').IEntity[]; + getSurroundingEntities(range: number): import('./IEntity').IEntity[]; + getSurroundingEntities(range: number, type: number): import('./IEntity').IEntity[]; + isAlive(): boolean; + getTempData(key: string): any; + setTempData(key: string, value: any): void; + hasTempData(key: string): boolean; + removeTempData(key: string): void; + clearTempData(): void; + getTempDataKeys(): string[]; + getStoredData(key: string): any; + setStoredData(key: string, value: any): void; + hasStoredData(key: string): boolean; + removeStoredData(key: string): void; + clearStoredData(): void; + getStoredDataKeys(): string[]; + getAge(): number; + despawn(): void; + inWater(): boolean; + inLava(): boolean; + inFire(): boolean; + isBurning(): boolean; + setBurning(ticks: number): void; + extinguish(): void; + getTypeName(): string; + dropItem(item: import('../item/IItemStack').IItemStack): void; + getRider(): import('./IEntity').IEntity; + setRider(entity: import('./IEntity').IEntity): void; + getMount(): import('./IEntity').IEntity; + setMount(entity: import('./IEntity').IEntity): void; + getType(): number; + typeOf(type: number): boolean; + setRotation(rotation: number): void; + setRotation(rotationYaw: number, rotationPitch: number): void; + getRotation(): number; + setPitch(pitch: number): void; + getPitch(): number; + knockback(power: number, direction: number): void; + knockback(xpower: number, ypower: number, zpower: number, direction: number): void; + knockback(pos: import('../IPos').IPos, direction: number): void; + setImmune(ticks: number): void; + setInvisible(invisible: boolean): void; + setSneaking(sneaking: boolean): void; + setSprinting(sprinting: boolean): void; + hasCollided(): boolean; + hasCollidedVertically(): boolean; + hasCollidedHorizontally(): boolean; + capturesDrops(): boolean; + setCapturesDrops(capture: boolean): void; + setCapturedDrops(capturedDrops: IEntity { + + // Methods + getOwner(): string; + setOwner(name: string): void; + getThrower(): string; + setThrower(name: string): void; + getPickupDelay(): number; + setPickupDelay(delay: number): void; + getAge(): number; + setAge(age: number): void; + getLifeSpawn(): number; + setLifeSpawn(age: number): void; + getItem(): import('../item/IItemStack').IItemStack; + setItem(item: import('../item/IItemStack').IItemStack): void; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts new file mode 100644 index 000000000..fbb890558 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts @@ -0,0 +1,33 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IEntityLiving extends IEntityLivingBase { + + // Methods + isNavigating(): boolean; + clearNavigation(): void; + navigateTo(x: number, y: number, z: number, speed: number): void; + getMCEntity(): T; + playLivingSound(): void; + spawnExplosionParticle(): void; + setMoveForward(speed: number): void; + faceEntity(entity: import('./IEntity').IEntity, pitch: number, yaw: number): void; + canPickUpLoot(): boolean; + setCanPickUpLoot(pickUp: boolean): void; + isPersistent(): boolean; + enablePersistence(): void; + setCustomNameTag(text: string): void; + getCustomNameTag(): string; + hasCustomNameTag(): boolean; + setAlwaysRenderNameTag(alwaysRender: boolean): void; + getAlwaysRenderNameTag(): boolean; + clearLeashed(sendPacket: boolean, dropLeash: boolean): void; + allowLeashing(): boolean; + getLeashed(): boolean; + getLeashedTo(): import('./IEntity').IEntity; + setLeashedTo(entity: import('./IEntity').IEntity, sendPacket: boolean): void; + canBeSteered(): boolean; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts new file mode 100644 index 000000000..3a9aef6ce --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts @@ -0,0 +1,65 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IEntityLivingBase extends import('./IEntity').IEntity { + + // Methods + getHealth(): number; + setHealth(health: number): void; + hurt(damage: number): void; + hurt(damage: number, source: import('./IEntity').IEntity): void; + hurt(damage: number, damageSource: import('../IDamageSource').IDamageSource): void; + setMaxHurtTime(time: number): void; + getMaxHurtTime(): number; + getMaxHealth(): number; + getFollowRange(): number; + getKnockbackResistance(): number; + getSpeed(): number; + getMeleeStrength(): number; + setMaxHealth(health: number): void; + setFollowRange(range: number): void; + setKnockbackResistance(knockbackResistance: number): void; + setSpeed(speed: number): void; + setMeleeStrength(attackDamage: number): void; + isAttacking(): boolean; + setAttackTarget(living: IEntityLivingBase): void; + getAttackTarget(): IEntityLivingBase; + getAttackTargetTime(): number; + setLastAttacker(p_130011_1_: import('./IEntity').IEntity): void; + getLastAttacker(): import('./IEntity').IEntity; + getLastAttackerTime(): number; + canBreatheUnderwater(): boolean; + getType(): number; + typeOf(type: number): boolean; + getLookVector(): import('../IPos').IPos; + getLookingAtBlock(maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('../IBlock').IBlock; + getLookingAtBlock(maxDistance: number): import('../IBlock').IBlock; + getLookingAtPos(maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('../IPos').IPos; + getLookingAtPos(maxDistance: number): import('../IPos').IPos; + getLookingAtEntities(ignoreEntities: import('./IEntity').IEntity[], maxDistance: number, offset: number, range: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IEntity').IEntity[]; + getLookingAtEntities(maxDistance: number, offset: number, range: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IEntity').IEntity[]; + getLookingAtEntities(maxDistance: number, offset: number, range: number): import('./IEntity').IEntity[]; + getMCEntity(): T; + swingHand(): void; + addPotionEffect(effect: number, duration: number, strength: number, hideParticles: boolean): void; + clearPotionEffects(): void; + getPotionEffect(effect: number): number; + getHeldItem(): import('../item/IItemStack').IItemStack; + setHeldItem(item: import('../item/IItemStack').IItemStack): void; + getArmor(slot: number): import('../item/IItemStack').IItemStack; + setArmor(slot: number, item: import('../item/IItemStack').IItemStack): void; + isChild(): boolean; + renderBrokenItemStack(itemStack: import('../item/IItemStack').IItemStack): void; + isOnLadder(): boolean; + getTotalArmorValue(): number; + getArrowCountInEntity(): number; + setArrowCountInEntity(count: number): void; + dismountEntity(entity: import('./IEntity').IEntity): void; + setAIMoveSpeed(speed: number): void; + getAIMoveSpeed(): number; + setAbsorptionAmount(amount: number): void; + getAbsorptionAmount(): number; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts new file mode 100644 index 000000000..4b1c9b09f --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts @@ -0,0 +1,12 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IFishHook extends IEntity { + + // Methods + getCaster(): import('./IPlayer').IPlayer; + kill(): void; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts new file mode 100644 index 000000000..10c4632d1 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts @@ -0,0 +1,8 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IMonster extends IEntityLiving { + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts new file mode 100644 index 000000000..b397fb06c --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts @@ -0,0 +1,33 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IPixelmon extends IAnimal { + + // Methods + getIsShiny(): boolean; + setIsShiny(bo: boolean): void; + getLevel(): number; + setLevel(level: number): void; + getIV(type: number): number; + setIV(type: number, value: number): void; + getEV(type: number): number; + setEV(type: number, value: number): void; + getStat(type: number): number; + setStat(type: number, value: number): void; + getSize(): number; + setSize(type: number): void; + getHapiness(): number; + setHapiness(value: number): void; + getNature(): number; + setNature(type: number): void; + getPokeball(): number; + setPokeball(type: number): void; + getNickname(): string; + hasNickname(): boolean; + setNickname(name: string): void; + getMove(slot: number): string; + setMove(slot: number, move: string): void; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts new file mode 100644 index 000000000..efeaafc75 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts @@ -0,0 +1,106 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IPlayer extends IEntityLivingBase, IAnimatable { + + // Methods + getDisplayName(): string; + getName(): string; + kick(reason: string): void; + setPosition(x: number, y: number, z: number): void; + setPosition(pos: import('../IPos').IPos): void; + setPosition(x: number, y: number, z: number, dimensionId: number): void; + setPosition(pos: import('../IPos').IPos, dimensionId: number): void; + setPosition(x: number, y: number, z: number, world: import('../IWorld').IWorld): void; + setPosition(pos: import('../IPos').IPos, world: import('../IWorld').IWorld): void; + setDimension(dimension: number): void; + getHunger(): number; + setHunger(hunger: number): void; + getSaturation(): number; + setSaturation(saturation: number): void; + showDialog(dialog: import('../handler/data/IDialog').IDialog): void; + hasReadDialog(dialog: import('../handler/data/IDialog').IDialog): boolean; + readDialog(dialog: import('../handler/data/IDialog').IDialog): void; + unreadDialog(dialog: import('../handler/data/IDialog').IDialog): void; + showDialog(id: number): void; + hasReadDialog(id: number): boolean; + readDialog(id: number): void; + unreadDialog(id: number): void; + hasFinishedQuest(quest: import('../handler/data/IQuest').IQuest): boolean; + hasActiveQuest(quest: import('../handler/data/IQuest').IQuest): boolean; + startQuest(quest: import('../handler/data/IQuest').IQuest): void; + finishQuest(quest: import('../handler/data/IQuest').IQuest): void; + stopQuest(quest: import('../handler/data/IQuest').IQuest): void; + removeQuest(quest: import('../handler/data/IQuest').IQuest): void; + hasFinishedQuest(id: number): boolean; + hasActiveQuest(id: number): boolean; + startQuest(id: number): void; + finishQuest(id: number): void; + stopQuest(id: number): void; + removeQuest(id: number): void; + getFinishedQuests(): import('../handler/data/IQuest').IQuest[]; + getType(): number; + typeOf(type: number): boolean; + addFactionPoints(faction: number, points: number): void; + setFactionPoints(faction: number, points: number): void; + getFactionPoints(faction: number): number; + sendMessage(message: string): void; + getMode(): number; + setMode(type: number): void; + getInventory(): import('../item/IItemStack').IItemStack[]; + inventoryItemCount(item: import('../item/IItemStack').IItemStack, ignoreNBT: boolean, ignoreDamage: boolean): number; + removeItem(id: string, damage: number, amount: number): boolean; + removeItem(item: import('../item/IItemStack').IItemStack, amount: number, ignoreNBT: boolean, ignoreDamage: boolean): boolean; + removeAllItems(item: import('../item/IItemStack').IItemStack, ignoreNBT: boolean, ignoreDamage: boolean): number; + giveItem(item: import('../item/IItemStack').IItemStack, amount: number): boolean; + giveItem(id: string, damage: number, amount: number): boolean; + setSpawnpoint(x: number, y: number, z: number): void; + setSpawnpoint(pos: import('../IPos').IPos): void; + resetSpawnpoint(): void; + setRotation(rotationYaw: number, rotationPitch: number): void; + disableMouseInput(time: number, buttonIds: ): void; + stopUsingItem(): void; + clearItemInUse(): void; + clearInventory(): void; + playSound(name: string, volume: number, pitch: number): void; + playSound(id: number, sound: import('../handler/data/ISound').ISound): void; + playSound(sound: import('../handler/data/ISound').ISound): void; + stopSound(id: number): void; + pauseSounds(): void; + continueSounds(): void; + stopSounds(): void; + mountEntity(ridingEntity: Entity): void; + dropOneItem(dropStack: boolean): import('./IEntity').IEntity; + canHarvestBlock(block: import('../IBlock').IBlock): boolean; + interactWith(entity: import('./IEntity').IEntity): boolean; + hasAchievement(achievement: string): boolean; + hasBukkitPermission(permission: string): boolean; + getExpLevel(): number; + setExpLevel(level: number): void; + getPixelmonData(): import('../IPixelmonPlayerData').IPixelmonPlayerData; + getTimers(): import('../ITimers').ITimers; + updatePlayerInventory(): void; + getDBCPlayer(): import('../../kamkeel/npcdbc/api/IDBCAddon').IDBCAddon; + blocking(): boolean; + getData(): import('../handler/IPlayerData').IPlayerData; + isScriptingDev(): boolean; + getActiveQuests(): import('../handler/data/IQuest').IQuest[]; + getOpenContainer(): import('../IContainer').IContainer; + showCustomGui(gui: import('../gui/ICustomGui').ICustomGui): void; + getCustomGui(): import('../gui/ICustomGui').ICustomGui; + closeGui(): void; + showCustomOverlay(overlay: import('../overlay/ICustomOverlay').ICustomOverlay): void; + closeOverlay(id: number): void; + getOverlays(): import('../handler/IOverlayHandler').IOverlayHandler; + getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; + setConqueredEnd(conqueredEnd: boolean): void; + conqueredEnd(): boolean; + getScreenSize(): import('../IScreenSize').IScreenSize; + getMagicData(): import('../handler/data/IMagicData').IMagicData; + getAttributes(): import('../handler/data/IPlayerAttributes').IPlayerAttributes; + getActionManager(): import('../handler/IActionManager').IActionManager; + getPartyMembers(): import('./IPlayer').IPlayer[]; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts new file mode 100644 index 000000000..d3142f27e --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts @@ -0,0 +1,21 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IProjectile { + + // Methods + getItem(): import('../item/IItemStack').IItemStack; + setItem(item: import('../item/IItemStack').IItemStack): void; + getHasGravity(): boolean; + setHasGravity(bo: boolean): void; + getAccuracy(): number; + setAccuracy(accuracy: number): void; + setHeading(entity: import('./IEntity').IEntity): void; + setHeading(x: number, y: number, z: number): void; + setHeading(yaw: number, pitch: number): void; + getThrower(): import('./IEntity').IEntity; + enableEvents(): void; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts new file mode 100644 index 000000000..3eb2842f9 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts @@ -0,0 +1,12 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IThrowable extends IEntity { + + // Methods + getThrower(): IEntityLivingBase; + kill(): void; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts new file mode 100644 index 000000000..c9a444f4d --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts @@ -0,0 +1,13 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity + */ + +export interface IVillager extends IEntityLiving { + + // Methods + getProfession(): number; + getIsTrading(): boolean; + getCustomer(): IEntityLivingBase; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts new file mode 100644 index 000000000..d7b466ec2 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts @@ -0,0 +1,16 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity.data + */ + +export interface IMark { + + // Methods + getAvailability(): import('../../handler/data/IAvailability').IAvailability; + getColor(): number; + setColor(color: number): void; + getType(): number; + setType(type: number): void; + update(): void; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts new file mode 100644 index 000000000..78239eb82 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts @@ -0,0 +1,30 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity.data + */ + +export interface IModelData { + + // Methods + headWear(config: byte): void; + headWear(): byte; + bodyWear(config: byte): void; + bodyWear(): byte; + rightArmWear(config: byte): void; + rightArmWear(): byte; + leftArmWear(config: byte): void; + leftArmWear(): byte; + rightLegWear(config: byte): void; + rightLegWear(): byte; + leftLegWear(config: byte): void; + leftLegWear(): byte; + hidePart(part: number, hide: byte): void; + hidden(part: number): number; + enableRotation(enableRotation: boolean): void; + enableRotation(): boolean; + getRotation(): import('./IModelRotate').IModelRotate; + getScale(): import('./IModelScale').IModelScale; + setEntity(string: string): void; + getEntity(): string; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts new file mode 100644 index 000000000..c873533a7 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts @@ -0,0 +1,17 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity.data + */ + +export interface IModelRotate { + + // Methods + whileStanding(): boolean; + whileStanding(whileStanding: boolean): void; + whileAttacking(): boolean; + whileAttacking(whileAttacking: boolean): void; + whileMoving(): boolean; + whileMoving(whileMoving: boolean): void; + getPart(part: number): import('./IModelRotatePart').IModelRotatePart; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts new file mode 100644 index 000000000..58df0518b --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts @@ -0,0 +1,16 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity.data + */ + +export interface IModelRotatePart { + + // Methods + setRotation(x: number, y: number, z: number): void; + getRotateX(): number; + getRotateY(): number; + getRotateZ(): number; + disabled(enabled: boolean): void; + disabled(): boolean; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts new file mode 100644 index 000000000..a47b29f59 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts @@ -0,0 +1,11 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity.data + */ + +export interface IModelScale { + + // Methods + getPart(part: number): import('./IModelScalePart').IModelScalePart; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts new file mode 100644 index 000000000..d0c089eb8 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts @@ -0,0 +1,14 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.entity.data + */ + +export interface IModelScalePart { + + // Methods + setScale(x: number, y: number, z: number): void; + getScaleX(): number; + getScaleY(): number; + getScaleZ(): number; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts new file mode 100644 index 000000000..906f120e5 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts @@ -0,0 +1,27 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IAnimationEvent extends ICustomNPCsEvent { + + // Methods + getAnimation(): import('../handler/data/IAnimation').IAnimation; + getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; + getEntity(): import('../entity/IAnimatable').IAnimatable; + + // Nested interfaces + interface Started extends IAnimationEvent { + } + interface Ended extends IAnimationEvent { + } + interface IFrameEvent extends IAnimationEvent { + getIndex(): number; + getFrame(): import('../handler/data/IFrame').IFrame; + } + interface Entered extends IFrameEvent { + } + interface Exited extends IFrameEvent { + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts new file mode 100644 index 000000000..38f60b47d --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts @@ -0,0 +1,51 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IBlockEvent extends ICustomNPCsEvent { + getBlock(): import('../IBlock').IBlock; +} + +export namespace IBlockEvent { + export interface EntityFallenUponEvent extends IBlockEvent { + getEntity(): import('../entity/IEntity').IEntity; + getDistanceFallen(): number; + } + export interface InteractEvent extends IBlockEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getHitX(): number; + getHitY(): number; + getHitZ(): number; + getSide(): number; + } + export interface RedstoneEvent extends IBlockEvent { + getPrevPower(): number; + getPower(): number; + } + export interface BreakEvent extends IBlockEvent { + } + export interface ExplodedEvent extends IBlockEvent { + } + export interface RainFillEvent extends IBlockEvent { + } + export interface NeighborChangedEvent extends IBlockEvent { + getChangedPos(): import('../IPos').IPos; + } + export interface InitEvent extends IBlockEvent { + } + export interface UpdateEvent extends IBlockEvent { + } + export interface ClickedEvent extends IBlockEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface HarvestedEvent extends IBlockEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface CollidedEvent extends IBlockEvent { + getEntity(): import('../entity/IEntity').IEntity; + } + export interface TimerEvent extends IBlockEvent { + getId(): number; + } +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts new file mode 100644 index 000000000..ae87a3319 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts @@ -0,0 +1,32 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface ICustomGuiEvent extends IPlayerEvent { + + // Methods + getGui(): import('../gui/ICustomGui').ICustomGui; + getId(): number; + + // Nested interfaces + interface ButtonEvent extends ICustomGuiEvent { + } + interface UnfocusedEvent extends ICustomGuiEvent { + } + interface CloseEvent extends ICustomGuiEvent { + } + interface ScrollEvent extends ICustomGuiEvent { + getSelection(): string[]; + doubleClick(): boolean; + getScrollIndex(): number; + } + interface SlotEvent extends ICustomGuiEvent { + getStack(): import('../item/IItemStack').IItemStack; + } + interface SlotClickEvent extends ICustomGuiEvent { + getStack(): import('../item/IItemStack').IItemStack; + getDragType(): number; + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts new file mode 100644 index 000000000..d09f119fb --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts @@ -0,0 +1,29 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface ICustomNPCsEvent { + getHookName(): string; +} + +export namespace ICustomNPCsEvent { + export interface CNPCNaturalSpawnEvent extends ICustomNPCsEvent { + getNaturalSpawn(): import('../handler/data/INaturalSpawn').INaturalSpawn; + setAttemptPosition(attemptPosition: import('../IPos').IPos): void; + getAttemptPosition(): import('../IPos').IPos; + animalSpawnPassed(): boolean; + monsterSpawnPassed(): boolean; + liquidSpawnPassed(): boolean; + airSpawnPassed(): boolean; + } + export interface ScriptedCommandEvent extends ICustomNPCsEvent { + getSenderWorld(): import('../IWorld').IWorld; + getSenderPosition(): import('../IPos').IPos; + getSenderName(): string; + setReplyMessage(message: string): void; + getId(): string; + getArgs(): string[]; + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts new file mode 100644 index 000000000..ad4624e5d --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts @@ -0,0 +1,19 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IDialogEvent extends IPlayerEvent { + + // Methods + getDialog(): import('../handler/data/IDialog').IDialog; + + // Nested interfaces + interface DialogOpen extends IDialogEvent { + } + interface DialogOption extends IDialogEvent { + } + interface DialogClosed extends IDialogEvent { + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts new file mode 100644 index 000000000..f2e62ec13 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts @@ -0,0 +1,17 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IFactionEvent extends IPlayerEvent { + + // Methods + getFaction(): import('../handler/data/IFaction').IFaction; + + // Nested interfaces + interface FactionPoints extends IFactionEvent { + decreased(): boolean; + getPoints(): number; + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts new file mode 100644 index 000000000..454e3963c --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts @@ -0,0 +1,19 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IForgeEvent extends ICustomNPCsEvent { + getEvent(): any; +} + +export namespace IForgeEvent { + export interface WorldEvent extends IForgeEvent { + getWorld(): import('../IWorld').IWorld; + } + export interface EntityEvent extends IForgeEvent { + getEntity(): import('../entity/IEntity').IEntity; + } + export interface InitEvent extends IForgeEvent { + } +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts new file mode 100644 index 000000000..58a1cd805 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts @@ -0,0 +1,66 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IItemEvent extends ICustomNPCsEvent { + getItem(): import('../item/IItemCustomizable').IItemCustomizable; +} + +export namespace IItemEvent { + export type InitEvent = IItemEvent + export interface UpdateEvent extends IItemEvent { + getEntity(): import('../entity/IEntity').IEntity; + } + export interface TossedEvent extends IItemEvent { + getEntity(): import('../entity/IEntity').IEntity; + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface PickedUpEvent extends IItemEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface SpawnEvent extends IItemEvent { + getEntity(): import('../entity/IEntity').IEntity; + } + export interface InteractEvent extends IItemEvent { + getType(): number; + getTarget(): import('../entity/IEntity').IEntity; + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface RightClickEvent extends IItemEvent { + getType(): number; + getTarget(): any; + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface AttackEvent extends IItemEvent { + getType(): number; + getTarget(): import('../entity/IEntity').IEntity; + getSwingingEntity(): import('../entity/IEntity').IEntity; + } + export interface StartUsingItem extends IItemEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getDuration(): number; + } + export interface UsingItem extends IItemEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getDuration(): number; + } + export interface StopUsingItem extends IItemEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getDuration(): number; + } + export interface FinishUsingItem extends IItemEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getDuration(): number; + } + export interface BreakItem extends IItemEvent { + getBrokenStack(): import('../item/IItemStack').IItemStack; + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface RepairItem extends IItemEvent { + getLeft(): import('../item/IItemStack').IItemStack; + getRight(): import('../item/IItemStack').IItemStack; + getOutput(): import('../item/IItemStack').IItemStack; + getAnvilBreakChance(): number; + } +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts new file mode 100644 index 000000000..b2274486e --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts @@ -0,0 +1,16 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface ILinkedItemEvent extends IItemEvent { + + // Nested interfaces + interface VersionChangeEvent extends IItemEvent { + getVersion(): number; + getPreviousVersion(): number; + } + interface BuildEvent extends IItemEvent { + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts new file mode 100644 index 000000000..742aae138 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts @@ -0,0 +1,79 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface INpcEvent extends ICustomNPCsEvent { + getNpc(): import('../entity/ICustomNpc').ICustomNpc; +} + +export namespace INpcEvent { + export interface TimerEvent extends INpcEvent { + getId(): number; + } + export interface CollideEvent extends INpcEvent { + getEntity(): import('../entity/IEntity').IEntity; + } + export interface DamagedEvent extends INpcEvent { + getSource(): import('../entity/IEntity').IEntity; + getDamageSource(): import('../IDamageSource').IDamageSource; + getDamage(): number; + setDamage(damage: number): void; + setClearTarget(bo: boolean): void; + getClearTarget(): boolean; + getType(): string; + } + export interface RangedLaunchedEvent extends INpcEvent { + getTarget(): IEntityLivingBase; + setDamage(damage: number): void; + getDamage(): number; + } + export interface MeleeAttackEvent extends INpcEvent { + getTarget(): IEntityLivingBase; + setDamage(damage: number): void; + getDamage(): number; + } + export interface SwingEvent extends INpcEvent { + getItemStack(): import('../item/IItemStack').IItemStack; + } + export interface KilledEntityEvent extends INpcEvent { + getEntity(): IEntityLivingBase; + } + export interface DiedEvent extends INpcEvent { + getSource(): import('../entity/IEntity').IEntity; + getDamageSource(): import('../IDamageSource').IDamageSource; + getType(): string; + setDroppedItems(droppedItems: import('../item/IItemStack').IItemStack[]): void; + getDroppedItems(): import('../item/IItemStack').IItemStack[]; + setExpDropped(expDropped: number): void; + getExpDropped(): number; + } + export interface InteractEvent extends INpcEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + } + export interface DialogEvent extends INpcEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getDialog(): import('../handler/data/IDialog').IDialog; + getDialogId(): number; + getOptionId(): number; + } + export interface DialogClosedEvent extends INpcEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getDialog(): import('../handler/data/IDialog').IDialog; + getDialogId(): number; + getOptionId(): number; + } + export interface TargetLostEvent extends INpcEvent { + getTarget(): IEntityLivingBase; + getNewTarget(): IEntityLivingBase; + } + export interface TargetEvent extends INpcEvent { + setTarget(entity: IEntityLivingBase): void; + getTarget(): IEntityLivingBase; + } + export interface UpdateEvent extends INpcEvent { + } + export interface InitEvent extends INpcEvent { + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts new file mode 100644 index 000000000..200eb3567 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts @@ -0,0 +1,34 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IPartyEvent { + + // Methods + getParty(): import('../handler/data/IParty').IParty; + getQuest(): import('../handler/data/IQuest').IQuest; + + // Nested interfaces + interface PartyQuestCompletedEvent extends IPartyEvent { + } + interface PartyQuestSetEvent extends IPartyEvent { + } + interface PartyQuestTurnedInEvent extends IPartyEvent { + } + interface PartyInviteEvent extends IPartyEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getPlayerName(): string; + } + interface PartyKickEvent extends IPartyEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getPlayerName(): string; + } + interface PartyLeaveEvent extends IPartyEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; + getPlayerName(): string; + } + interface PartyDisbandEvent extends IPartyEvent { + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts new file mode 100644 index 000000000..e7b6366ca --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts @@ -0,0 +1,191 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IPlayerEvent extends ICustomNPCsEvent { + getPlayer(): import('../entity/IPlayer').IPlayer; +} + +export namespace IPlayerEvent { + export interface ChatEvent extends IPlayerEvent { + setMessage(message: string): void; + getMessage(): string; + } + export interface KeyPressedEvent extends IPlayerEvent { + getKey(): number; + isCtrlPressed(): boolean; + isAltPressed(): boolean; + isShiftPressed(): boolean; + isMetaPressed(): boolean; + keyDown(): boolean; + getKeysDown(): number[]; + } + export interface MouseClickedEvent extends IPlayerEvent { + getButton(): number; + getMouseWheel(): number; + buttonDown(): boolean; + isCtrlPressed(): boolean; + isAltPressed(): boolean; + isShiftPressed(): boolean; + isMetaPressed(): boolean; + getKeysDown(): number[]; + } + export interface PickupXPEvent extends IPlayerEvent { + getAmount(): number; + } + export interface LevelUpEvent extends IPlayerEvent { + getChange(): number; + } + export type LogoutEvent = IPlayerEvent + export type LoginEvent = IPlayerEvent + export type RespawnEvent = IPlayerEvent + export interface ChangedDimension extends IPlayerEvent { + getFromDim(): number; + getToDim(): number; + } + export interface TimerEvent extends IPlayerEvent { + getId(): number; + } + export interface AttackedEvent extends IPlayerEvent { + getDamageSource(): import('../IDamageSource').IDamageSource; + getSource(): import('../entity/IEntity').IEntity; + getDamage(): number; + } + export interface DamagedEvent extends IPlayerEvent { + getDamageSource(): import('../IDamageSource').IDamageSource; + getSource(): import('../entity/IEntity').IEntity; + getDamage(): number; + } + export type LightningEvent = IPlayerEvent + export interface SoundEvent extends IPlayerEvent { + getName(): string; + getPitch(): number; + getVolume(): number; + } + export interface FallEvent extends IPlayerEvent { + getDistance(): number; + } + export type JumpEvent = IPlayerEvent + export interface KilledEntityEvent extends IPlayerEvent { + getEntity(): IEntityLivingBase; + } + export interface DiedEvent extends IPlayerEvent { + getDamageSource(): import('../IDamageSource').IDamageSource; + getType(): string; + getSource(): import('../entity/IEntity').IEntity; + } + export interface RangedLaunchedEvent extends IPlayerEvent { + getBow(): import('../item/IItemStack').IItemStack; + getCharge(): number; + } + export interface AttackEvent extends IPlayerEvent { + getDamageSource(): import('../IDamageSource').IDamageSource; + getTarget(): import('../entity/IEntity').IEntity; + getDamage(): number; + } + export interface DamagedEntityEvent extends IPlayerEvent { + getDamageSource(): import('../IDamageSource').IDamageSource; + getTarget(): import('../entity/IEntity').IEntity; + getDamage(): number; + } + export interface ContainerClosed extends IPlayerEvent { + getContainer(): import('../IContainer').IContainer; + } + export interface ContainerOpen extends IPlayerEvent { + getContainer(): import('../IContainer').IContainer; + } + export interface PickUpEvent extends IPlayerEvent { + getItem(): import('../item/IItemStack').IItemStack; + } + export interface DropEvent extends IPlayerEvent { + getItems(): import('../item/IItemStack').IItemStack[]; + } + export interface TossEvent extends IPlayerEvent { + getItem(): import('../item/IItemStack').IItemStack; + } + export interface InteractEvent extends IPlayerEvent { + getType(): number; + getTarget(): import('../entity/IEntity').IEntity; + } + export interface RightClickEvent extends IPlayerEvent { + getType(): number; + getTarget(): any; + + } + export type UpdateEvent = IPlayerEvent + export type InitEvent = IPlayerEvent + export interface StartUsingItem extends IPlayerEvent { + getItem(): import('../item/IItemStack').IItemStack; + getDuration(): number; + } + export interface UsingItem extends IPlayerEvent { + getItem(): import('../item/IItemStack').IItemStack; + getDuration(): number; + } + export interface StopUsingItem extends IPlayerEvent { + getItem(): import('../item/IItemStack').IItemStack; + getDuration(): number; + } + export interface FinishUsingItem extends IPlayerEvent { + getItem(): import('../item/IItemStack').IItemStack; + getDuration(): number; + } + export interface BreakEvent extends IPlayerEvent { + getBlock(): import('../IBlock').IBlock; + getExp(): number; + } + export interface UseHoeEvent extends IPlayerEvent { + getHoe(): import('../item/IItemStack').IItemStack; + getX(): number; + getY(): number; + getZ(): number; + } + export interface WakeUpEvent extends IPlayerEvent { + setSpawn(): boolean; + } + export interface SleepEvent extends IPlayerEvent { + getX(): number; + getY(): number; + getZ(): number; + } + export interface AchievementEvent extends IPlayerEvent { + getDescription(): string; + } + export interface FillBucketEvent extends IPlayerEvent { + getCurrent(): import('../item/IItemStack').IItemStack; + getFilled(): import('../item/IItemStack').IItemStack; + } + export interface BonemealEvent extends IPlayerEvent { + getBlock(): import('../IBlock').IBlock; + getX(): number; + getY(): number; + getZ(): number; + } + export type RangedChargeEvent = IPlayerEvent + export interface EffectEvent extends IPlayerEvent { + getEffect(): import('../handler/data/IPlayerEffect').IPlayerEffect; + } + export namespace EffectEvent { + export type Added = EffectEvent + export type Ticked = EffectEvent + export interface Removed extends EffectEvent { + hasTimerRunOut(): boolean; + causedByDeath(): boolean; + } + } + + export interface ProfileEvent extends IPlayerEvent { + getProfile(): import('../handler/data/IProfile').IProfile; + getSlot(): number; + isPost(): boolean; + } + export namespace ProfileEvent { + export interface Changed extends ProfileEvent { + getPrevSlot(): number; + } + export type Create = ProfileEvent + export type Removed = ProfileEvent + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts new file mode 100644 index 000000000..69debbbd0 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts @@ -0,0 +1,21 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IProjectileEvent extends ICustomNPCsEvent { + + // Methods + getProjectile(): import('../entity/IProjectile').IProjectile; + getSource(): import('../entity/IEntity').IEntity; + + // Nested interfaces + interface UpdateEvent extends IProjectileEvent { + } + interface ImpactEvent extends IProjectileEvent { + getType(): number; + getEntity(): import('../entity/IEntity').IEntity; + getBlock(): import('../IBlock').IBlock; + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts new file mode 100644 index 000000000..a5fa1d3d5 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts @@ -0,0 +1,23 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IQuestEvent extends IPlayerEvent { + + // Methods + getQuest(): import('../handler/data/IQuest').IQuest; + + // Nested interfaces + interface QuestCompletedEvent extends IQuestEvent { + } + interface QuestStartEvent extends IQuestEvent { + } + interface QuestTurnedInEvent extends IQuestEvent { + setExpReward(expReward: number): void; + setItemRewards(itemRewards: import('../item/IItemStack').IItemStack[]): void; + getExpReward(): number; + getItemRewards(): import('../item/IItemStack').IItemStack[]; + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts new file mode 100644 index 000000000..6c6ad56b3 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts @@ -0,0 +1,33 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.event + */ + +export interface IRecipeEvent extends IPlayerEvent { + + // Methods + getRecipe(): any; + getItems(): import('../item/IItemStack').IItemStack[]; + isAnvil(): boolean; + setMessage(message: string): void; + getMessage(): string; + getXpCost(): number; + setXpCost(xpCost: number): void; + getMaterialUsage(): number; + setMaterialUsage(materialUsage: number): void; + + // Nested interfaces + interface Pre extends IRecipeEvent { + setMessage(message: string): void; + getMessage(): string; + getXpCost(): number; + setXpCost(xpCost: number): void; + getMaterialUsage(): number; + setMaterialUsage(materialUsage: number): void; + } + interface Post extends IRecipeEvent { + getCraft(): import('../item/IItemStack').IItemStack; + setResult(stack: import('../item/IItemStack').IItemStack): void; + } + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts new file mode 100644 index 000000000..df117e8c9 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts @@ -0,0 +1,25 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.gui + */ + +export interface IButton extends ICustomGuiComponent { + + // Methods + getWidth(): number; + getHeight(): number; + setSize(width: number, height: number): import('./IButton').IButton; + getLabel(): string; + setLabel(text: string): import('./IButton').IButton; + getTexture(): string; + hasTexture(): boolean; + setTexture(texture: string): import('./IButton').IButton; + getTextureX(): number; + getTextureY(): number; + setTextureOffset(textureX: number, textureY: number): import('./IButton').IButton; + setScale(scale: number): void; + getScale(): number; + setEnabled(enabled: boolean): void; + isEnabled(): boolean; + +} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts new file mode 100644 index 000000000..4c0dd8d86 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts @@ -0,0 +1,47 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: noppes.npcs.api.gui + */ + +export interface ICustomGui { + + // Methods + getID(): number; + getWidth(): number; + getHeight(): number; + getComponents(): Array +/// +/** + * Ambient declarations for `scripts_examples/item_events.js`. + * These map the function names used by CustomNPC+ item scripts to the + * precise event interface so `event.` autocompletes correctly inside + * each handler. This file is intended to be referenced from the script + * with a triple-slash reference comment. + */ + +declare function init(IItemEvent: IItemEvent.InitEvent): void; +declare function tick(IItemEvent: IItemEvent.UpdateEvent): void; +declare function tossed(IItemEvent: IItemEvent.TossedEvent): void; +declare function pickedUp(IItemEvent: IItemEvent.PickedUpEvent): void; +declare function spawn(IItemEvent: IItemEvent.SpawnEvent): void; +declare function interact(IItemEvent: IItemEvent.InteractEvent): void; +declare function rightClick(IItemEvent: IItemEvent.RightClickEvent): void; +declare function attack(IItemEvent: IItemEvent.AttackEvent): void; +declare function startItem(IItemEvent: IItemEvent.StartUsingItem): void; +declare function usingItem(IItemEvent: IItemEvent.UsingItem): void; +declare function stopItem(IItemEvent: IItemEvent.StopUsingItem): void; +declare function finishItem(IItemEvent: IItemEvent.FinishUsingItem): void; + +export {}; diff --git a/src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts b/src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts new file mode 100644 index 000000000..f8e21f6f0 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts @@ -0,0 +1,32 @@ +/// +/// +/** + * Ambient declarations for `scripts_examples/npc_events.js`. + * These map the function names used by CustomNPC+ NPC scripts to the + * precise event interface so `event.` autocompletes correctly inside + * each handler. Parameter names determine type inference: + * - Use `INpcEvent` for NPC-specific events + * - Use `IProjectileEvent` for projectile events + */ + +declare function init(INpcEvent: INpcEvent.InitEvent): void; +declare function tick(INpcEvent: INpcEvent.UpdateEvent): void; +declare function interact(INpcEvent: INpcEvent.InteractEvent): void; +declare function dialog(INpcEvent: INpcEvent.DialogEvent): void; +declare function damaged(INpcEvent: INpcEvent.DamagedEvent): void; +declare function killed(INpcEvent: INpcEvent.DiedEvent): void; +declare function meleeAttack(INpcEvent: INpcEvent.MeleeAttackEvent): void; +declare function meleeSwing(INpcEvent: INpcEvent.SwingEvent): void; +declare function rangedLaunched(INpcEvent: INpcEvent.RangedLaunchedEvent): void; +declare function target(INpcEvent: INpcEvent.TargetEvent): void; +declare function collide(INpcEvent: INpcEvent.CollideEvent): void; +declare function kills(INpcEvent: INpcEvent.KilledEntityEvent): void; +declare function dialogClose(INpcEvent: INpcEvent.DialogClosedEvent): void; +declare function timer(INpcEvent: INpcEvent.TimerEvent): void; +declare function targetLost(INpcEvent: INpcEvent.TargetLostEvent): void; + +// Projectile-specific +declare function projectileTick(IProjectileEvent: IProjectileEvent.UpdateEvent): void; +declare function projectileImpact(IProjectileEvent: IProjectileEvent.ImpactEvent): void; + +export {}; diff --git a/src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts b/src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts new file mode 100644 index 000000000..e8e7669b2 --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts @@ -0,0 +1,92 @@ +/// +/// +/** + * Ambient declarations for `scripts_examples/player_events.js`. + * These map the function names used by CustomNPC+ player scripts to the + * precise event interface so `event.` autocompletes correctly inside + * each handler. Parameter names determine type inference: + * - Use `IPlayerEvent` for player-specific events + * - Use `IQuestEvent`, `IFactionEvent`, `IDialogEvent`, etc. for specialized event types + */ + +declare function init(IPlayerEvent: IPlayerEvent.InitEvent): void; +declare function tick(IPlayerEvent: IPlayerEvent.UpdateEvent): void; +declare function interact(IPlayerEvent: IPlayerEvent.InteractEvent): void; +declare function rightClick(IPlayerEvent: IPlayerEvent.RightClickEvent): void; +declare function attack(IPlayerEvent: IPlayerEvent.AttackEvent): void; +declare function attacked(IPlayerEvent: IPlayerEvent.AttackedEvent): void; +declare function damagedEntity(IPlayerEvent: IPlayerEvent.DamagedEntityEvent): void; +declare function damaged(IPlayerEvent: IPlayerEvent.DamagedEvent): void; +declare function kills(IPlayerEvent: IPlayerEvent.KilledEntityEvent): void; +declare function killed(IPlayerEvent: IPlayerEvent.DiedEvent): void; +declare function drop(IPlayerEvent: IPlayerEvent.DropEvent): void; +declare function respawn(IPlayerEvent: IPlayerEvent.RespawnEvent): void; +declare function breakBlock(IPlayerEvent: IPlayerEvent.BreakEvent): void; +declare function chat(IPlayerEvent: IPlayerEvent.ChatEvent): void; +declare function login(IPlayerEvent: IPlayerEvent.LoginEvent): void; +declare function logout(IPlayerEvent: IPlayerEvent.LogoutEvent): void; +declare function keyPressed(IPlayerEvent: IPlayerEvent.KeyPressedEvent): void; +declare function mouseClicked(IPlayerEvent: IPlayerEvent.MouseClickedEvent): void; +declare function toss(IPlayerEvent: IPlayerEvent.TossEvent): void; +declare function pickUp(IPlayerEvent: IPlayerEvent.PickUpEvent): void; +declare function pickupXP(IPlayerEvent: IPlayerEvent.PickupXPEvent): void; +declare function rangedCharge(IPlayerEvent: IPlayerEvent.RangedChargeEvent): void; +declare function rangedLaunched(IPlayerEvent: IPlayerEvent.RangedLaunchedEvent): void; +declare function timer(IPlayerEvent: IPlayerEvent.TimerEvent): void; +declare function startItem(IPlayerEvent: IPlayerEvent.StartUsingItem): void; +declare function usingItem(IPlayerEvent: IPlayerEvent.UsingItem): void; +declare function stopItem(IPlayerEvent: IPlayerEvent.StopUsingItem): void; +declare function finishItem(IPlayerEvent: IPlayerEvent.FinishUsingItem): void; +declare function containerOpen(IPlayerEvent: IPlayerEvent.ContainerOpen): void; +declare function useHoe(IPlayerEvent: IPlayerEvent.UseHoeEvent): void; +declare function bonemeal(IPlayerEvent: IPlayerEvent.BonemealEvent): void; +declare function fillBucket(IPlayerEvent: IPlayerEvent.FillBucketEvent): void; +declare function jump(IPlayerEvent: IPlayerEvent.JumpEvent): void; +declare function fall(IPlayerEvent: IPlayerEvent.FallEvent): void; +declare function wakeUp(IPlayerEvent: IPlayerEvent.WakeUpEvent): void; +declare function sleep(IPlayerEvent: IPlayerEvent.SleepEvent): void; +declare function playSound(IPlayerEvent: IPlayerEvent.SoundEvent): void; +declare function lightning(IPlayerEvent: IPlayerEvent.LightningEvent): void; +declare function changedDim(IPlayerEvent: IPlayerEvent.ChangedDimension): void; + +// Quest / faction / dialog / custom events +declare function questStart(IQuestEvent: IQuestEvent.QuestStartEvent): void; +declare function questCompleted(IQuestEvent: IQuestEvent.QuestCompletedEvent): void; +declare function questTurnIn(IQuestEvent: IQuestEvent.QuestTurnedInEvent): void; +declare function factionPoints(IFactionEvent: IFactionEvent.FactionPoints): void; + +declare function dialogOpen(IDialogEvent: IDialogEvent.DialogClosed): void; +declare function dialogOption(IDialogEvent: IDialogEvent.DialogOption): void; +declare function dialogClose(IDialogEvent: IDialogEvent.DialogClosed): void; + +declare function scriptCommand(ICustomNPCsEvent: ICustomNPCsEvent.ScriptedCommandEvent): void; +declare function customGuiClosed(ICustomGuiEvent: ICustomGuiEvent.CloseEvent): void; +declare function customGuiButton(ICustomGuiEvent: ICustomGuiEvent.ButtonEvent): void; +declare function customGuiSlot(ICustomGuiEvent: ICustomGuiEvent.SlotEvent): void; +declare function customGuiSlotClicked(ICustomGuiEvent: ICustomGuiEvent.SlotClickEvent): void; +declare function customGuiScroll(ICustomGuiEvent: ICustomGuiEvent.ScrollEvent): void; + +// Party events +declare function partyQuestCompleted(IPartyEvent: IPartyEvent.PartyQuestCompletedEvent): void; +declare function partyQuestSet(IPartyEvent: IPartyEvent.PartyQuestSetEvent): void; +declare function partyQuestTurnedIn(IPartyEvent: IPartyEvent.PartyQuestTurnedInEvent): void; +declare function partyInvite(IPartyEvent: IPartyEvent.PartyInviteEvent): void; +declare function partyKick(IPartyEvent: IPartyEvent.PartyKickEvent): void; +declare function partyLeave(IPartyEvent: IPartyEvent.PartyLeaveEvent): void; +declare function partyDisband(IPartyEvent: IPartyEvent.PartyDisbandEvent): void; + +// Animation events +declare function animationStart(IAnimationEvent: IAnimationEvent.Started): void; +declare function animationEnd(IAnimationEvent: IAnimationEvent.Ended): void; +declare function frameEnter(IAnimationEvent: IAnimationEvent.IFrameEvent): void; +declare function frameExit(IAnimationEvent: IAnimationEvent.IFrameEvent): void; + +// Profile / effect events +declare function profileChange(IPlayerEvent: IPlayerEvent.ProfileEvent): void; +declare function profileRemove(IPlayerEvent: IPlayerEvent.ProfileEvent): void; +declare function profileCreate(IPlayerEvent: IPlayerEvent.ProfileEvent): void; +declare function onEffectAdd(IPlayerEvent: IPlayerEvent.EffectEvent): void; +declare function onEffectTick(IPlayerEvent: IPlayerEvent.EffectEvent): void; +declare function onEffectRemove(IPlayerEvent: IPlayerEvent.EffectEvent): void; + +export {}; From 2361246f0bca75a3aff3880cd4ac827a2fe064f2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 14:23:35 +0200 Subject: [PATCH 161/337] LMAO Autocompletion Bar LMFAO --- src/api | 2 +- .../client/gui/util/GuiScriptTextArea.java | 116 +++- .../script/autocomplete/AutocompleteItem.java | 442 ++++++++++++++ .../autocomplete/AutocompleteManager.java | 561 ++++++++++++++++++ .../script/autocomplete/AutocompleteMenu.java | 488 +++++++++++++++ .../autocomplete/AutocompleteProvider.java | 63 ++ .../autocomplete/JSAutocompleteProvider.java | 288 +++++++++ .../JavaAutocompleteProvider.java | 287 +++++++++ .../script/interpreter/ScriptDocument.java | 47 +- .../interpreter/js_parser/JSTypeRegistry.java | 20 + .../client/key/impl/ScriptEditorKeys.java | 3 + 11 files changed, 2310 insertions(+), 7 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java diff --git a/src/api b/src/api index d003d6d87..305a3778c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit d003d6d870db13d56fce223050a06faf15050eae +Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 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 55ab59f97..32aac7680 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -20,6 +20,7 @@ import noppes.npcs.client.gui.util.script.interpreter.hover.GutterIconRenderer; 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.util.ValueUtil; import org.lwjgl.input.Keyboard; @@ -120,6 +121,9 @@ private int getPaddedLineCount() { // ==================== RENAME REFACTOR ==================== private final RenameRefactorHandler renameHandler = new RenameRefactorHandler(); + // ==================== AUTOCOMPLETE ==================== + private final AutocompleteManager autocompleteManager = new AutocompleteManager(); + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -461,6 +465,56 @@ public int getViewportWidth() { return width - LINE_NUMBER_GUTTER_WIDTH - 8; // Account for gutter and scrollbar } }); + + // Initialize Autocomplete Manager with callback + autocompleteManager.setInsertCallback(new AutocompleteManager.InsertCallback() { + @Override + public void insertText(String text, int startPosition) { + // Replace text from startPosition to current cursor + int cursorPos = selection.getCursorPosition(); + String before = GuiScriptTextArea.this.text.substring(0, startPosition); + String after = GuiScriptTextArea.this.text.substring(cursorPos); + setText(before + text + after); + selection.reset(startPosition + text.length()); + scrollToCursor(); + } + + @Override + public int getCursorPosition() { + return selection.getCursorPosition(); + } + + @Override + public String getText() { + return GuiScriptTextArea.this.text; + } + + @Override + public int[] getCursorScreenPosition() { + // Calculate screen position of cursor for menu placement + int cursorLine = getCursorLineIndex(); + int cursorCol = 0; + if (container != null && container.lines != null && cursorLine < container.lines.size()) { + LineData ld = container.lines.get(cursorLine); + String lineText = ld.text; + int cursorOffset = selection.getCursorPosition() - ld.start; + cursorCol = ClientProxy.Font.width(lineText.substring(0, Math.min(cursorOffset, lineText.length()))); + } + + int screenX = GuiScriptTextArea.this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + cursorCol; + int lineY = cursorLine - scroll.getScrolledLine(); + int screenY = GuiScriptTextArea.this.y + lineY * (container != null ? container.lineHeight : 12); + + return new int[] { screenX, screenY }; + } + + @Override + public int[] getViewportDimensions() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + return new int[] { sr.getScaledWidth(), sr.getScaledHeight() }; + } + }); } public boolean fullscreen() { @@ -507,9 +561,15 @@ public void drawTextBox(int xMouse, int yMouse) { int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { ((GuiNPCInterface) listener).mouseScroll = wheelDelta; - boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; - if (wheelDelta != 0 && canScroll) - scroll.applyWheelScroll(wheelDelta, maxScroll); + + // Let autocomplete menu consume scroll first if visible + if (wheelDelta != 0 && autocompleteManager.isVisible() && autocompleteManager.mouseScrolled(xMouse, yMouse, wheelDelta)) { + // Autocomplete consumed the scroll + } else { + boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; + if (wheelDelta != 0 && canScroll) + scroll.applyWheelScroll(wheelDelta, maxScroll); + } } // Handle scrollbar dragging (delegated to ScrollState) @@ -901,6 +961,10 @@ public void drawTextBox(int xMouse, int yMouse) { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); + + // Draw autocomplete menu (overlays code area) + autocompleteManager.draw(xMouse, yMouse); + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); // Draw hover tooltips (on top of everything) @@ -1322,6 +1386,14 @@ private void initializeKeyBindings() { renameHandler.startRename(); } }); + + // AUTOCOMPLETE: Trigger autocomplete (Ctrl+Space) + KEYS.AUTOCOMPLETE.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + autocompleteManager.triggerExplicit(); + }); } public void unfocusAll() { @@ -1329,6 +1401,8 @@ public void unfocusAll() { if (goToLineDialog.hasFocus()) goToLineDialog.unfocus(); if (renameHandler.isActive()) renameHandler.cancel(); + if (autocompleteManager.isVisible()) + autocompleteManager.dismiss(); } // ==================== KEYBOARD INPUT HANDLING ==================== @@ -1352,6 +1426,13 @@ public boolean textboxKeyTyped(char c, int i) { // Handle search bar input first if it has focus if (searchBar.isVisible() && searchBar.keyTyped(c, i)) return true; + + // Handle autocomplete navigation keys first when visible + if (autocompleteManager.isVisible()) { + if (autocompleteManager.keyPressed(i)) { + return true; + } + } if (!active) return false; @@ -1743,6 +1824,8 @@ private boolean handleDeletionKeys(int i) { setText(s + getSelectionAfterText()); selection.reset(selection.getStartSelection()); scrollToCursor(); + // Notify autocomplete of deletion + autocompleteManager.onDeleteKey(text, selection.getCursorPosition()); return true; } @@ -1897,6 +1980,8 @@ private boolean handleCharacterInput(char c) { // Move caret forward by one (skip over existing closer) selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } @@ -1906,30 +1991,40 @@ private boolean handleCharacterInput(char c) { setText(before + "\"\"" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } if (c == '\'') { setText(before + "''" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } if (c == '[') { setText(before + "[]" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } if (c == '(') { setText(before + "()" + after); selection.reset(before.length() + 1); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } // Default insertion for printable characters: insert at caret (replacing selection) addText(Character.toString(c)); scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); return true; } return false; @@ -1958,7 +2053,7 @@ private void toggleCommentLineAtCursor() { } public boolean closeOnEsc(){ - return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive() && !autocompleteManager.isVisible(); } // ==================== KEYBOARD MODIFIERS ==================== @@ -2154,6 +2249,16 @@ public String getSelectionAfterText() { // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + // Check autocomplete menu clicks first + if (autocompleteManager.isVisible() && autocompleteManager.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + + // Dismiss autocomplete if clicking elsewhere + if (autocompleteManager.isVisible()) { + autocompleteManager.dismiss(); + } + // Check go to line dialog clicks first if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { return; @@ -2311,6 +2416,9 @@ public void setText(String text) { // Consider text changes user activity to pause caret blinking briefly selection.markActivity(); searchBar.updateMatches(); + + // Update autocomplete manager with current container + autocompleteManager.setContainer(this.container); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java new file mode 100644 index 000000000..79140da21 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -0,0 +1,442 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents a single autocomplete suggestion. + * Unified representation for both Java and JavaScript completions. + */ +public class AutocompleteItem implements Comparable { + + /** + * The kind of completion item. + */ + public enum Kind { + METHOD(0), // Method or function + FIELD(1), // Field or property + VARIABLE(2), // Local variable or parameter + CLASS(3), // Class or interface type + ENUM(4), // Enum type + ENUM_CONSTANT(5), // Enum constant value + KEYWORD(6), // Language keyword + SNIPPET(7); // Code snippet + + private final int priority; + + Kind(int priority) { + this.priority = priority; + } + + public int getPriority() { + return priority; + } + } + + private final String name; // Display name (e.g., "getPlayer") + private final String insertText; // Text to insert (e.g., "getPlayer()") + private final Kind kind; // Type of completion + private final String typeLabel; // Type label (e.g., "IPlayer", "void") + private final String signature; // Full signature for methods + private final String documentation; // Documentation/description + private final Object sourceData; // Original source (MethodInfo, FieldInfo, etc.) + private final boolean deprecated; // Whether this item is deprecated + + // Match scoring + private int matchScore = 0; // How well this matches the query + private int[] matchIndices; // Indices of matched characters for highlighting + + private AutocompleteItem(String name, String insertText, Kind kind, String typeLabel, + String signature, String documentation, Object sourceData, boolean deprecated) { + this.name = name; + this.insertText = insertText; + this.kind = kind; + this.typeLabel = typeLabel; + this.signature = signature; + this.documentation = documentation; + this.sourceData = sourceData; + this.deprecated = deprecated; + } + + // ==================== FACTORY METHODS ==================== + + /** + * Create from a Java MethodInfo. + */ + public static AutocompleteItem fromMethod(MethodInfo method) { + String name = method.getName(); + StringBuilder insertText = new StringBuilder(name); + insertText.append("("); + + // Add placeholders for parameters if any + if (method.getParameterCount() > 0) { + // Just add () - user will fill in params + } + insertText.append(")"); + + String returnType = method.getReturnType() != null ? + method.getReturnType().getSimpleName() : "void"; + + String signature = buildMethodSignature(method); + + return new AutocompleteItem( + name, + insertText.toString(), + Kind.METHOD, + returnType, + signature, + method.getDocumentation(), + method, + false // TODO: Check for @Deprecated annotation + ); + } + + /** + * Create from a Java FieldInfo. + */ + public static AutocompleteItem fromField(FieldInfo field) { + String typeLabel = field.getTypeInfo() != null ? + field.getTypeInfo().getSimpleName() : "?"; + + Kind kind; + switch (field.getScope()) { + case PARAMETER: + kind = Kind.VARIABLE; + break; + case LOCAL: + kind = Kind.VARIABLE; + break; + case ENUM_CONSTANT: + kind = Kind.ENUM_CONSTANT; + break; + default: + kind = Kind.FIELD; + } + + return new AutocompleteItem( + field.getName(), + field.getName(), + kind, + typeLabel, + typeLabel + " " + field.getName(), + null, + field, + false + ); + } + + /** + * Create from a Java TypeInfo. + */ + public static AutocompleteItem fromType(TypeInfo type) { + Kind kind; + switch (type.getKind()) { + case INTERFACE: + kind = Kind.CLASS; + break; + case ENUM: + kind = Kind.ENUM; + break; + default: + kind = Kind.CLASS; + } + + return new AutocompleteItem( + type.getSimpleName(), + type.getSimpleName(), + kind, + type.getPackageName(), + type.getFullName(), + null, + type, + false + ); + } + + /** + * Create from a JavaScript JSMethodInfo. + */ + public static AutocompleteItem fromJSMethod(JSMethodInfo method) { + String name = method.getName(); + StringBuilder insertText = new StringBuilder(name); + insertText.append("("); + insertText.append(")"); + + return new AutocompleteItem( + name, + insertText.toString(), + Kind.METHOD, + method.getReturnType(), + method.getSignature(), + method.getDocumentation(), + method, + false + ); + } + + /** + * Create from a JavaScript JSFieldInfo. + */ + public static AutocompleteItem fromJSField(JSFieldInfo field) { + return new AutocompleteItem( + field.getName(), + field.getName(), + Kind.FIELD, + field.getType(), + field.toString(), + field.getDocumentation(), + field, + false + ); + } + + /** + * Create a keyword item. + */ + public static AutocompleteItem keyword(String keyword) { + return new AutocompleteItem( + keyword, + keyword, + Kind.KEYWORD, + "keyword", + null, + null, + null, + false + ); + } + + // ==================== HELPER METHODS ==================== + + private static String buildMethodSignature(MethodInfo method) { + StringBuilder sb = new StringBuilder(); + String returnType = method.getReturnType() != null ? + method.getReturnType().getSimpleName() : "void"; + sb.append(returnType).append(" ").append(method.getName()).append("("); + + for (int i = 0; i < method.getParameterCount(); i++) { + if (i > 0) sb.append(", "); + FieldInfo param = method.getParameters().get(i); + String paramType = param.getTypeInfo() != null ? + param.getTypeInfo().getSimpleName() : "?"; + sb.append(paramType).append(" ").append(param.getName()); + } + + sb.append(")"); + return sb.toString(); + } + + // ==================== MATCHING ==================== + + /** + * Calculate fuzzy match score against a query string. + * Returns -1 if no match, or a positive score (higher = better match). + */ + public int calculateMatchScore(String query) { + if (query == null || query.isEmpty()) { + matchScore = 100; // Everything matches empty query + matchIndices = new int[0]; + return matchScore; + } + + String lowerName = name.toLowerCase(); + String lowerQuery = query.toLowerCase(); + + // Exact prefix match is best + if (lowerName.startsWith(lowerQuery)) { + matchScore = 1000 - query.length(); // Shorter prefixes score higher + matchIndices = new int[query.length()]; + for (int i = 0; i < query.length(); i++) { + matchIndices[i] = i; + } + return matchScore; + } + + // Exact substring match + int subIndex = lowerName.indexOf(lowerQuery); + if (subIndex >= 0) { + matchScore = 500 - subIndex; // Earlier substrings score higher + matchIndices = new int[query.length()]; + for (int i = 0; i < query.length(); i++) { + matchIndices[i] = subIndex + i; + } + return matchScore; + } + + // Fuzzy match - characters must appear in order + int[] indices = new int[query.length()]; + int nameIdx = 0; + int queryIdx = 0; + int gaps = 0; + int consecutiveBonus = 0; + int lastMatchIdx = -2; + + while (queryIdx < query.length() && nameIdx < name.length()) { + if (Character.toLowerCase(name.charAt(nameIdx)) == lowerQuery.charAt(queryIdx)) { + indices[queryIdx] = nameIdx; + + // Bonus for consecutive matches + if (nameIdx == lastMatchIdx + 1) { + consecutiveBonus += 10; + } + + // Bonus for matching at word boundaries (camelCase) + if (nameIdx == 0 || !Character.isLetterOrDigit(name.charAt(nameIdx - 1)) || + (Character.isUpperCase(name.charAt(nameIdx)) && nameIdx > 0 && + Character.isLowerCase(name.charAt(nameIdx - 1)))) { + consecutiveBonus += 20; + } + + lastMatchIdx = nameIdx; + queryIdx++; + } else { + gaps++; + } + nameIdx++; + } + + if (queryIdx < query.length()) { + // Didn't match all characters + matchScore = -1; + matchIndices = null; + return -1; + } + + matchScore = 100 + consecutiveBonus - gaps; + matchIndices = indices; + return matchScore; + } + + // ==================== GETTERS ==================== + + public String getName() { return name; } + public String getInsertText() { return insertText; } + public Kind getKind() { return kind; } + public String getTypeLabel() { return typeLabel; } + public String getSignature() { return signature; } + public String getDocumentation() { return documentation; } + public Object getSourceData() { return sourceData; } + public boolean isDeprecated() { return deprecated; } + public int getMatchScore() { return matchScore; } + public int[] getMatchIndices() { return matchIndices; } + + /** + * Get icon identifier based on kind. + */ + public String getIconId() { + switch (kind) { + case METHOD: return "m"; + case FIELD: return "f"; + case VARIABLE: return "v"; + case CLASS: return "C"; + case ENUM: return "E"; + case ENUM_CONSTANT: return "e"; + case KEYWORD: return "k"; + case SNIPPET: return "s"; + default: return "?"; + } + } + + /** + * Get color for the kind icon. + */ + public int getIconColor() { + switch (kind) { + case METHOD: return 0xFFB877DB; // Purple for methods + case FIELD: return 0xFF79C0FF; // Blue for fields + case VARIABLE: return 0xFF7EE787; // Green for variables + case CLASS: return 0xFFFFA657; // Orange for classes + case ENUM: return 0xFFFFD866; // Yellow for enums + case ENUM_CONSTANT: return 0xFFFFD866; + case KEYWORD: return 0xFFFF7B72; // Red for keywords + case SNIPPET: return 0xFFCCCCCC; // Gray for snippets + default: return 0xFFCCCCCC; + } + } + + @Override + public int compareTo(AutocompleteItem other) { + // First by match score (descending) + if (this.matchScore != other.matchScore) { + return other.matchScore - this.matchScore; + } + // Then by kind priority + if (this.kind.getPriority() != other.kind.getPriority()) { + return this.kind.getPriority() - other.kind.getPriority(); + } + // Finally alphabetically + return this.name.compareToIgnoreCase(other.name); + } + + @Override + public String toString() { + return name + " (" + kind + ")"; + } + + // ==================== BUILDER ==================== + + /** + * Builder for creating AutocompleteItem instances without source data. + */ + public static class Builder { + private String name; + private String insertText; + private Kind kind = Kind.FIELD; + private String typeLabel = ""; + private String signature; + private String documentation; + private Object sourceData; + private boolean deprecated = false; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder insertText(String insertText) { + this.insertText = insertText; + return this; + } + + public Builder kind(Kind kind) { + this.kind = kind; + return this; + } + + public Builder typeLabel(String typeLabel) { + this.typeLabel = typeLabel; + return this; + } + + public Builder signature(String signature) { + this.signature = signature; + return this; + } + + public Builder documentation(String documentation) { + this.documentation = documentation; + return this; + } + + public Builder sourceData(Object sourceData) { + this.sourceData = sourceData; + return this; + } + + public Builder deprecated(boolean deprecated) { + this.deprecated = deprecated; + return this; + } + + public AutocompleteItem build() { + if (insertText == null) { + insertText = name; + } + return new AutocompleteItem(name, insertText, kind, typeLabel, + signature, documentation, sourceData, deprecated); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java new file mode 100644 index 000000000..0af1a3024 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -0,0 +1,561 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Manages autocomplete state and coordinates between providers and UI. + * This is the main entry point for autocomplete functionality. + */ +public class AutocompleteManager { + + // ==================== CONSTANTS ==================== + + /** Characters that trigger autocomplete */ + private static final String TRIGGER_CHARS = "."; + + /** Characters that should close autocomplete */ + private static final String CLOSE_CHARS = ";{}()[]<>,\"'`"; + + /** Minimum characters before showing suggestions (for non-dot triggers) */ + private static final int MIN_PREFIX_LENGTH = 1; + + /** Pattern for identifier characters */ + private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*"); + + // ==================== STATE ==================== + + private final AutocompleteMenu menu; + private final JavaAutocompleteProvider javaProvider; + private final JSAutocompleteProvider jsProvider; + + private ScriptTextContainer container; + private ScriptDocument document; + + /** Whether autocomplete is currently active */ + private boolean active = false; + + /** The prefix being typed */ + private String currentPrefix = ""; + + /** Start position of the current prefix */ + private int prefixStartPosition = -1; + + /** Whether this was an explicit trigger (Ctrl+Space) */ + private boolean explicitTrigger = false; + + /** Callback for text insertion */ + private InsertCallback insertCallback; + + /** + * Callback interface for inserting autocomplete results. + */ + public interface InsertCallback { + /** + * Insert text, replacing from start to current cursor position. + * @param text Text to insert + * @param startPosition Position to start replacing from + */ + void insertText(String text, int startPosition); + + /** + * Get current cursor position. + */ + int getCursorPosition(); + + /** + * Get current document text. + */ + String getText(); + + /** + * Get cursor screen coordinates for menu positioning. + */ + int[] getCursorScreenPosition(); + + /** + * Get viewport dimensions. + */ + int[] getViewportDimensions(); + } + + // ==================== CONSTRUCTOR ==================== + + public AutocompleteManager() { + this.menu = new AutocompleteMenu(); + this.javaProvider = new JavaAutocompleteProvider(); + this.jsProvider = new JSAutocompleteProvider(); + + // Set up menu callback + menu.setCallback(new AutocompleteMenu.AutocompleteCallback() { + @Override + public void onItemSelected(AutocompleteItem item) { + handleItemSelected(item); + } + + @Override + public void onDismiss() { + active = false; + } + }); + } + + // ==================== CONFIGURATION ==================== + + /** + * Set the script container for type resolution. + */ + public void setContainer(ScriptTextContainer container) { + this.container = container; + if (container != null) { + this.document = container.getDocument(); + javaProvider.setDocument(document); + jsProvider.setDocument(document); + } + } + + /** + * Set the callback for text insertion. + */ + public void setInsertCallback(InsertCallback callback) { + this.insertCallback = callback; + } + + // ==================== TRIGGER LOGIC ==================== + + /** + * Called when a character is typed. + * Determines whether to show, update, or hide autocomplete. + */ + public void onCharTyped(char c, String text, int cursorPosition) { + // Check if we should close autocomplete + if (CLOSE_CHARS.indexOf(c) >= 0) { + dismiss(); + return; + } + + // Check if this is a trigger character + if (TRIGGER_CHARS.indexOf(c) >= 0) { + // Trigger after dot + triggerAfterDot(text, cursorPosition); + return; + } + + // Check if we're typing an identifier + if (Character.isJavaIdentifierPart(c)) { + if (active) { + // Update existing autocomplete + updatePrefix(text, cursorPosition); + } else if (Character.isJavaIdentifierStart(c)) { + // Potentially start new autocomplete + maybeStartAutocomplete(text, cursorPosition, false); + } + return; + } + + // Any other character closes autocomplete + if (active && !Character.isWhitespace(c)) { + dismiss(); + } + } + + /** + * Called when backspace/delete is pressed. + */ + public void onDeleteKey(String text, int cursorPosition) { + if (active) { + updatePrefix(text, cursorPosition); + + // Close if prefix is now empty and not after dot + if (currentPrefix.isEmpty() && !isAfterDot(text, cursorPosition)) { + dismiss(); + } + } + } + + /** + * Explicitly trigger autocomplete (Ctrl+Space). + */ + public void triggerExplicit() { + if (insertCallback == null) return; + + String text = insertCallback.getText(); + int cursorPosition = insertCallback.getCursorPosition(); + + explicitTrigger = true; + + // Check if after dot + if (isAfterDot(text, cursorPosition)) { + triggerAfterDot(text, cursorPosition); + } else { + maybeStartAutocomplete(text, cursorPosition, true); + } + } + + /** + * Trigger autocomplete after a dot is typed. + */ + private void triggerAfterDot(String text, int cursorPosition) { + // Find the receiver expression before the dot + int dotPos = cursorPosition - 1; + if (dotPos < 0 || text.charAt(dotPos) != '.') { + dotPos = text.lastIndexOf('.', cursorPosition - 1); + } + + if (dotPos < 0) return; + + String receiverExpr = findReceiverExpression(text, dotPos); + String prefix = findCurrentWord(text, cursorPosition); + + prefixStartPosition = dotPos + 1; + currentPrefix = prefix; + + showSuggestions(text, cursorPosition, prefix, prefixStartPosition, true, receiverExpr); + } + + /** + * Maybe start autocomplete based on current context. + */ + private void maybeStartAutocomplete(String text, int cursorPosition, boolean force) { + String prefix = findCurrentWord(text, cursorPosition); + int prefixStart = cursorPosition - prefix.length(); + + // Check minimum prefix length (unless forced) + if (!force && prefix.length() < MIN_PREFIX_LENGTH) { + return; + } + + // Don't auto-trigger if we're in the middle of a word + if (!force && prefixStart > 0 && Character.isJavaIdentifierPart(text.charAt(prefixStart - 1))) { + return; + } + + prefixStartPosition = prefixStart; + currentPrefix = prefix; + + showSuggestions(text, cursorPosition, prefix, prefixStart, false, null); + } + + /** + * Update the prefix and refresh suggestions. + */ + private void updatePrefix(String text, int cursorPosition) { + // Re-calculate prefix from the original start position + if (prefixStartPosition < 0 || prefixStartPosition > cursorPosition) { + dismiss(); + return; + } + + String newPrefix = text.substring(prefixStartPosition, cursorPosition); + + // Validate prefix (should be valid identifier or empty) + if (!newPrefix.isEmpty() && !isValidPrefix(newPrefix)) { + dismiss(); + return; + } + + currentPrefix = newPrefix; + + // Check if after dot + boolean isMemberAccess = prefixStartPosition > 0 && + text.charAt(prefixStartPosition - 1) == '.'; + + String receiverExpr = null; + if (isMemberAccess) { + receiverExpr = findReceiverExpression(text, prefixStartPosition - 1); + } + + showSuggestions(text, cursorPosition, currentPrefix, prefixStartPosition, + isMemberAccess, receiverExpr); + } + + // ==================== SUGGESTION LOGIC ==================== + + /** + * Show autocomplete suggestions. + */ + private void showSuggestions(String text, int cursorPosition, String prefix, + int prefixStart, boolean isMemberAccess, String receiverExpr) { + if (insertCallback == null || document == null) return; + + // Build context + int lineNumber = getLineNumber(text, cursorPosition); + String currentLine = getCurrentLine(text, cursorPosition); + int columnPosition = getColumnPosition(text, cursorPosition); + + AutocompleteProvider.Context context = new AutocompleteProvider.Context( + text, cursorPosition, lineNumber, columnPosition, currentLine, + prefix, prefixStart, isMemberAccess, receiverExpr, explicitTrigger + ); + + // Get suggestions from appropriate provider + AutocompleteProvider provider = document.isJavaScript() ? jsProvider : javaProvider; + List suggestions = provider.getSuggestions(context); + + // Show menu + if (suggestions.isEmpty()) { + if (explicitTrigger) { + // Show "No suggestions" message + suggestions.add(new AutocompleteItem.Builder() + .name("No suggestions") + .kind(AutocompleteItem.Kind.SNIPPET) + .typeLabel("") + .build()); + } else { + dismiss(); + return; + } + } + + // Get screen position for menu + int[] screenPos = insertCallback.getCursorScreenPosition(); + int[] viewport = insertCallback.getViewportDimensions(); + + menu.show(screenPos[0], screenPos[1] + 15, suggestions, viewport[0], viewport[1]); + active = true; + explicitTrigger = false; + } + + // ==================== KEY HANDLING ==================== + + /** + * Handle key press. + * @return true if the key was consumed + */ + public boolean keyPressed(int keyCode) { + if (!active || !menu.isVisible()) return false; + + switch (keyCode) { + case 200: // UP + menu.selectPrevious(); + return true; + + case 208: // DOWN + menu.selectNext(); + return true; + + case 28: // ENTER + case 15: // TAB + if (menu.hasItems()) { + menu.confirmSelection(); + return true; + } + return false; + + case 1: // ESCAPE + dismiss(); + return true; + + default: + return false; + } + } + + /** + * Handle mouse click. + * @return true if the click was consumed + */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!active) return false; + return menu.mouseClicked(mouseX, mouseY, button); + } + + /** + * Handle mouse scroll. + * @return true if the scroll was consumed + */ + public boolean mouseScrolled(int mouseX, int mouseY, int delta) { + if (!active) return false; + return menu.mouseScrolled(mouseX, mouseY, delta); + } + + // ==================== ITEM SELECTION ==================== + + /** + * Handle when an item is selected from the menu. + */ + private void handleItemSelected(AutocompleteItem item) { + if (insertCallback == null || item == null) return; + + // Don't insert "No suggestions" placeholder + if (item.getName().equals("No suggestions")) { + return; + } + + String insertText = item.getInsertText(); + insertCallback.insertText(insertText, prefixStartPosition); + + active = false; + } + + // ==================== DISMISS ==================== + + /** + * Dismiss autocomplete. + */ + public void dismiss() { + if (active) { + menu.hide(); + active = false; + currentPrefix = ""; + prefixStartPosition = -1; + explicitTrigger = false; + } + } + + // ==================== DRAWING ==================== + + /** + * Draw the autocomplete menu. + */ + public void draw(int mouseX, int mouseY) { + if (active) { + menu.draw(mouseX, mouseY); + } + } + + // ==================== STATE QUERIES ==================== + + public boolean isActive() { + return active; + } + + public boolean isVisible() { + return active && menu.isVisible(); + } + + public AutocompleteMenu getMenu() { + return menu; + } + + // ==================== HELPER METHODS ==================== + + /** + * Find the word being typed at the cursor position. + */ + private String findCurrentWord(String text, int cursorPos) { + if (text == null || cursorPos <= 0) return ""; + + int start = cursorPos; + while (start > 0 && Character.isJavaIdentifierPart(text.charAt(start - 1))) { + start--; + } + + return text.substring(start, cursorPos); + } + + /** + * Find the receiver expression before a dot. + */ + private String findReceiverExpression(String text, int dotPos) { + if (text == null || dotPos <= 0) return ""; + + int end = dotPos; + int start = end; + int parenDepth = 0; + int bracketDepth = 0; + + // Walk backwards to find the start of the expression + while (start > 0) { + char c = text.charAt(start - 1); + + if (c == ')') { + parenDepth++; + start--; + } else if (c == '(') { + if (parenDepth > 0) { + parenDepth--; + start--; + } else { + break; + } + } else if (c == ']') { + bracketDepth++; + start--; + } else if (c == '[') { + if (bracketDepth > 0) { + bracketDepth--; + start--; + } else { + break; + } + } else if (Character.isJavaIdentifierPart(c) || c == '.') { + start--; + } else if (Character.isWhitespace(c)) { + // Skip whitespace within expression (e.g., after method call) + if (parenDepth > 0 || bracketDepth > 0) { + start--; + } else { + break; + } + } else { + break; + } + } + + String expr = text.substring(start, end).trim(); + + // Clean up the expression + expr = expr.replaceAll("\\s+", ""); + + return expr; + } + + /** + * Check if cursor is after a dot. + */ + private boolean isAfterDot(String text, int cursorPos) { + // Look backwards, skipping any identifier characters + int pos = cursorPos - 1; + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) { + pos--; + } + return pos >= 0 && text.charAt(pos) == '.'; + } + + /** + * Check if a prefix is valid (identifier characters only). + */ + private boolean isValidPrefix(String prefix) { + if (prefix.isEmpty()) return true; + if (!Character.isJavaIdentifierStart(prefix.charAt(0))) return false; + for (int i = 1; i < prefix.length(); i++) { + if (!Character.isJavaIdentifierPart(prefix.charAt(i))) return false; + } + return true; + } + + /** + * Get line number (0-indexed) for a position. + */ + private int getLineNumber(String text, int position) { + int line = 0; + for (int i = 0; i < position && i < text.length(); i++) { + if (text.charAt(i) == '\n') line++; + } + return line; + } + + /** + * Get the current line text. + */ + private String getCurrentLine(String text, int position) { + int lineStart = text.lastIndexOf('\n', position - 1) + 1; + int lineEnd = text.indexOf('\n', position); + if (lineEnd < 0) lineEnd = text.length(); + return text.substring(lineStart, lineEnd); + } + + /** + * Get column position within the current line. + */ + private int getColumnPosition(String text, int position) { + int lineStart = text.lastIndexOf('\n', position - 1) + 1; + return position - lineStart; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java new file mode 100644 index 000000000..ea56e7deb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -0,0 +1,488 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.ScaledResolution; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * UI component for displaying autocomplete suggestions. + * Styled similar to SearchReplaceBar with IntelliJ-like appearance. + */ +public class AutocompleteMenu extends Gui { + + // ==================== CONSTANTS ==================== + private static final int MAX_VISIBLE_ITEMS = 10; + private static final int ITEM_HEIGHT = 16; + private static final int PADDING = 4; + private static final int ICON_WIDTH = 16; + private static final int MIN_WIDTH = 200; + private static final int MAX_WIDTH = 400; + private static final int HINT_HEIGHT = 18; + + // ==================== COLORS ==================== + private static final int BG_COLOR = 0xFF2d2d30; + private static final int BORDER_COLOR = 0xFF3c3c3c; + private static final int SELECTED_BG = 0xFF094771; + private static final int HOVER_BG = 0xFF37373d; + private static final int TEXT_COLOR = 0xFFe0e0e0; + private static final int DIM_TEXT_COLOR = 0xFF808080; + private static final int HIGHLIGHT_COLOR = 0xFFFFD866; + private static final int HINT_BG_COLOR = 0xFF252526; + private static final int SCROLLBAR_BG = 0xFF3c3c3c; + private static final int SCROLLBAR_FG = 0xFF606060; + + // ==================== STATE ==================== + private boolean visible = false; + private List items = new ArrayList<>(); + private int selectedIndex = 0; + private int scrollOffset = 0; + private int hoveredIndex = -1; + + // ==================== POSITION ==================== + private int x, y; + private int width, height; + private int menuWidth; + + // ==================== FONT ==================== + private final FontRenderer font; + + // ==================== CALLBACK ==================== + private AutocompleteCallback callback; + + /** + * Callback interface for autocomplete events. + */ + public interface AutocompleteCallback { + void onItemSelected(AutocompleteItem item); + void onDismiss(); + } + + public AutocompleteMenu() { + this.font = Minecraft.getMinecraft().fontRenderer; + } + + public void setCallback(AutocompleteCallback callback) { + this.callback = callback; + } + + // ==================== VISIBILITY ==================== + + /** + * Show the menu at the specified position with the given items. + */ + public void show(int x, int y, List items, int viewportWidth, int viewportHeight) { + this.items = items != null ? new ArrayList<>(items) : new ArrayList<>(); + this.selectedIndex = 0; + this.scrollOffset = 0; + this.hoveredIndex = -1; + + // Calculate menu dimensions + calculateDimensions(x, y, viewportWidth, viewportHeight); + + this.visible = !this.items.isEmpty(); + } + + /** + * Update the items while keeping the menu open. + */ + public void updateItems(List newItems) { + this.items = newItems != null ? new ArrayList<>(newItems) : new ArrayList<>(); + + // Clamp selection + if (selectedIndex >= items.size()) { + selectedIndex = Math.max(0, items.size() - 1); + } + + // Clamp scroll + int maxScroll = Math.max(0, items.size() - MAX_VISIBLE_ITEMS); + if (scrollOffset > maxScroll) { + scrollOffset = maxScroll; + } + + visible = !items.isEmpty(); + } + + /** + * Hide the menu. + */ + public void hide() { + visible = false; + if (callback != null) { + callback.onDismiss(); + } + } + + public boolean isVisible() { + return visible; + } + + public boolean hasItems() { + return !items.isEmpty(); + } + + // ==================== DIMENSIONS ==================== + + private void calculateDimensions(int cursorX, int cursorY, int viewportWidth, int viewportHeight) { + // Calculate width based on item content + int maxItemWidth = MIN_WIDTH; + for (AutocompleteItem item : items) { + int itemWidth = ICON_WIDTH + PADDING + font.getStringWidth(item.getName()) + PADDING; + if (item.getTypeLabel() != null) { + itemWidth += font.getStringWidth(item.getTypeLabel()) + PADDING * 2; + } + maxItemWidth = Math.max(maxItemWidth, itemWidth); + } + menuWidth = Math.min(maxItemWidth + 20, MAX_WIDTH); // +20 for scrollbar + + // Calculate height + int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); + int menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2; + + // Position below cursor, or above if not enough space + this.x = cursorX; + this.y = cursorY; + + // Check if menu would go off-screen to the right + if (x + menuWidth > viewportWidth - 10) { + x = viewportWidth - menuWidth - 10; + } + + // Check if menu would go off-screen to the bottom + if (y + menuHeight > viewportHeight - 10) { + // Show above cursor instead + y = cursorY - menuHeight - 20; // 20 for line height + } + + // Ensure not off-screen to the left or top + x = Math.max(5, x); + y = Math.max(5, y); + + this.width = menuWidth; + this.height = menuHeight; + } + + // ==================== SELECTION ==================== + + /** + * Move selection up. + */ + public void selectPrevious() { + if (items.isEmpty()) return; + + selectedIndex--; + if (selectedIndex < 0) { + selectedIndex = items.size() - 1; + scrollOffset = Math.max(0, items.size() - MAX_VISIBLE_ITEMS); + } else if (selectedIndex < scrollOffset) { + scrollOffset = selectedIndex; + } + } + + /** + * Move selection down. + */ + public void selectNext() { + if (items.isEmpty()) return; + + selectedIndex++; + if (selectedIndex >= items.size()) { + selectedIndex = 0; + scrollOffset = 0; + } else if (selectedIndex >= scrollOffset + MAX_VISIBLE_ITEMS) { + scrollOffset = selectedIndex - MAX_VISIBLE_ITEMS + 1; + } + } + + /** + * Confirm the current selection. + */ + public void confirmSelection() { + if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) { + hide(); + return; + } + + AutocompleteItem selected = items.get(selectedIndex); + if (callback != null) { + callback.onItemSelected(selected); + } + hide(); + } + + /** + * Get the currently selected item. + */ + public AutocompleteItem getSelectedItem() { + if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) { + return null; + } + return items.get(selectedIndex); + } + + // ==================== DRAWING ==================== + + /** + * Draw the autocomplete menu. + */ + public void draw(int mouseX, int mouseY) { + if (!visible || items.isEmpty()) return; + + // Update hover state + updateHoverState(mouseX, mouseY); + + // Enable scissoring to clip content + GL11.glEnable(GL11.GL_SCISSOR_TEST); + setScissor(x - 2, y - 2, width + 4, height + 4); + + // Draw background + drawRect(x, y, x + width, y + height, BG_COLOR); + + // Draw border + drawRect(x, y, x + width, y + 1, BORDER_COLOR); + drawRect(x, y + height - 1, x + width, y + height, BORDER_COLOR); + drawRect(x, y, x + 1, y + height, BORDER_COLOR); + drawRect(x + width - 1, y, x + width, y + height, BORDER_COLOR); + + // Draw items + int itemY = y + PADDING; + int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); + + for (int i = 0; i < visibleItems; i++) { + int itemIndex = scrollOffset + i; + if (itemIndex >= items.size()) break; + + AutocompleteItem item = items.get(itemIndex); + boolean isSelected = (itemIndex == selectedIndex); + boolean isHovered = (itemIndex == hoveredIndex); + + drawItem(item, x + PADDING, itemY, width - PADDING * 2 - 8, isSelected, isHovered); + itemY += ITEM_HEIGHT; + } + + // Draw scrollbar if needed + if (items.size() > MAX_VISIBLE_ITEMS) { + drawScrollbar(); + } + + // Draw hint bar at bottom + drawHintBar(); + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + /** + * Draw a single autocomplete item. + */ + private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth, + boolean selected, boolean hovered) { + // Background + if (selected) { + drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, SELECTED_BG); + } else if (hovered) { + drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, HOVER_BG); + } + + int textX = itemX; + int textY = itemY + (ITEM_HEIGHT - font.FONT_HEIGHT) / 2; + + // Draw icon + String icon = item.getIconId(); + int iconColor = item.getIconColor(); + font.drawString(icon, textX + (ICON_WIDTH - font.getStringWidth(icon)) / 2, textY, iconColor); + textX += ICON_WIDTH; + + // Draw name with match highlighting + drawHighlightedText(item.getName(), item.getMatchIndices(), textX, textY, + item.isDeprecated() ? DIM_TEXT_COLOR : TEXT_COLOR); + + // Draw type label on the right + if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) { + String typeLabel = item.getTypeLabel(); + int typeLabelWidth = font.getStringWidth(typeLabel); + int typeLabelX = itemX + itemWidth - typeLabelWidth - PADDING; + font.drawString(typeLabel, typeLabelX, textY, DIM_TEXT_COLOR); + } + } + + /** + * Draw text with specific characters highlighted. + */ + private void drawHighlightedText(String text, int[] matchIndices, int x, int y, int baseColor) { + if (matchIndices == null || matchIndices.length == 0) { + font.drawString(text, x, y, baseColor); + return; + } + + // Create a set of highlighted indices for quick lookup + java.util.Set highlightSet = new java.util.HashSet<>(); + for (int idx : matchIndices) { + highlightSet.add(idx); + } + + // Draw character by character + int currentX = x; + for (int i = 0; i < text.length(); i++) { + String ch = String.valueOf(text.charAt(i)); + int color = highlightSet.contains(i) ? HIGHLIGHT_COLOR : baseColor; + font.drawString(ch, currentX, y, color); + currentX += font.getStringWidth(ch); + } + } + + /** + * Draw the scrollbar. + */ + private void drawScrollbar() { + int scrollbarX = x + width - 8; + int scrollbarY = y + PADDING; + int scrollbarHeight = MAX_VISIBLE_ITEMS * ITEM_HEIGHT; + + // Background + drawRect(scrollbarX, scrollbarY, scrollbarX + 6, scrollbarY + scrollbarHeight, SCROLLBAR_BG); + + // Thumb + float thumbRatio = (float) MAX_VISIBLE_ITEMS / items.size(); + int thumbHeight = Math.max(20, (int) (scrollbarHeight * thumbRatio)); + float thumbPosRatio = (float) scrollOffset / Math.max(1, items.size() - MAX_VISIBLE_ITEMS); + int thumbY = scrollbarY + (int) ((scrollbarHeight - thumbHeight) * thumbPosRatio); + + drawRect(scrollbarX + 1, thumbY, scrollbarX + 5, thumbY + thumbHeight, SCROLLBAR_FG); + } + + /** + * Draw the hint bar at the bottom (shows Tab/Enter to confirm). + */ + private void drawHintBar() { + int hintY = y + height - HINT_HEIGHT; + + // Darker background for hint area + drawRect(x + 1, hintY, x + width - 1, y + height - 1, HINT_BG_COLOR); + + // Draw hints + int hintTextY = hintY + (HINT_HEIGHT - font.FONT_HEIGHT) / 2; + int hintX = x + PADDING; + + // Tab hint + drawKeyHint("Tab", hintX, hintTextY); + hintX += font.getStringWidth("Tab") + 8; + font.drawString("or", hintX, hintTextY, DIM_TEXT_COLOR); + hintX += font.getStringWidth("or") + 4; + + // Enter hint + drawKeyHint("Enter", hintX, hintTextY); + hintX += font.getStringWidth("Enter") + 8; + font.drawString("to insert", hintX, hintTextY, DIM_TEXT_COLOR); + } + + /** + * Draw a key hint with a darker background box. + */ + private void drawKeyHint(String key, int x, int y) { + int keyWidth = font.getStringWidth(key); + int boxPadding = 2; + + // Draw box + drawRect(x - boxPadding, y - boxPadding, + x + keyWidth + boxPadding, y + font.FONT_HEIGHT + boxPadding, + 0xFF404040); + + // Draw key text + font.drawString(key, x, y, TEXT_COLOR); + } + + // ==================== MOUSE HANDLING ==================== + + /** + * Update hover state based on mouse position. + */ + private void updateHoverState(int mouseX, int mouseY) { + hoveredIndex = -1; + + if (!isMouseInBounds(mouseX, mouseY)) return; + + int itemY = y + PADDING; + int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); + + for (int i = 0; i < visibleItems; i++) { + if (mouseY >= itemY && mouseY < itemY + ITEM_HEIGHT) { + hoveredIndex = scrollOffset + i; + break; + } + itemY += ITEM_HEIGHT; + } + } + + /** + * Handle mouse click. + * @return true if click was consumed + */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Check if click is outside menu + if (!isMouseInBounds(mouseX, mouseY)) { + hide(); + return false; + } + + // Check if clicking on an item + if (button == 0 && hoveredIndex >= 0 && hoveredIndex < items.size()) { + selectedIndex = hoveredIndex; + confirmSelection(); + return true; + } + + return true; // Consume click if inside menu + } + + /** + * Handle mouse scroll. + * @return true if scroll was consumed + */ + public boolean mouseScrolled(int mouseX, int mouseY, int delta) { + if (!visible || !isMouseInBounds(mouseX, mouseY)) return false; + + if (items.size() > MAX_VISIBLE_ITEMS) { + if (delta > 0) { + scrollOffset = Math.max(0, scrollOffset - 1); + } else { + scrollOffset = Math.min(items.size() - MAX_VISIBLE_ITEMS, scrollOffset + 1); + } + return true; + } + + return false; + } + + private boolean isMouseInBounds(int mouseX, int mouseY) { + return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; + } + + // ==================== UTILITY ==================== + + private void setScissor(int x, int y, int width, int height) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + + int scissorX = x * scaleFactor; + int scissorY = (sr.getScaledHeight() - y - height) * scaleFactor; + int scissorW = width * scaleFactor; + int scissorH = height * scaleFactor; + + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + } + + // ==================== GETTERS ==================== + + public int getX() { return x; } + public int getY() { return y; } + public int getWidth() { return width; } + public int getHeight() { return height; } + public List getItems() { return items; } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java new file mode 100644 index 000000000..1ada3614a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java @@ -0,0 +1,63 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import java.util.List; + +/** + * Abstract provider for autocomplete suggestions. + * Implementations handle different contexts (after dot, in identifier, etc.) + */ +public interface AutocompleteProvider { + + /** + * Context information for autocomplete requests. + */ + class Context { + /** Full text of the document */ + public final String text; + /** Current cursor position in the document */ + public final int cursorPosition; + /** The current line number (0-indexed) */ + public final int lineNumber; + /** Position of cursor within the current line */ + public final int columnPosition; + /** The current line text */ + public final String currentLine; + /** The word being typed (prefix before cursor, after last separator) */ + public final String prefix; + /** Start position of the prefix in the document */ + public final int prefixStart; + /** Whether this is after a dot (member access) */ + public final boolean isMemberAccess; + /** Expression before the dot (for member access) */ + public final String receiverExpression; + /** Whether autocomplete was explicitly triggered (CTRL+Space) */ + public final boolean explicitTrigger; + + public Context(String text, int cursorPosition, int lineNumber, int columnPosition, + String currentLine, String prefix, int prefixStart, + boolean isMemberAccess, String receiverExpression, boolean explicitTrigger) { + this.text = text; + this.cursorPosition = cursorPosition; + this.lineNumber = lineNumber; + this.columnPosition = columnPosition; + this.currentLine = currentLine; + this.prefix = prefix; + this.prefixStart = prefixStart; + this.isMemberAccess = isMemberAccess; + this.receiverExpression = receiverExpression; + this.explicitTrigger = explicitTrigger; + } + } + + /** + * Get autocomplete suggestions for the given context. + * @param context The context containing cursor position, text, etc. + * @return List of autocomplete items, sorted by relevance + */ + List getSuggestions(Context context); + + /** + * Check if this provider can handle the given context. + */ + boolean canProvide(Context context); +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java new file mode 100644 index 000000000..fc536102c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -0,0 +1,288 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.*; + +import java.util.*; + +/** + * Autocomplete provider for JavaScript/ECMAScript scripts. + * Uses JSTypeRegistry for type resolution. + */ +public class JSAutocompleteProvider implements AutocompleteProvider { + + private ScriptDocument document; + private final JSTypeRegistry registry; + + // Variable types inferred from the document + private Map variableTypes = new HashMap<>(); + + public JSAutocompleteProvider() { + this.registry = JSTypeRegistry.getInstance(); + if (!registry.isInitialized()) { + registry.initializeFromResources(); + } + } + + public void setDocument(ScriptDocument document) { + this.document = document; + } + + /** + * Update the variable type map from document analysis. + */ + public void updateVariableTypes(Map types) { + this.variableTypes = types != null ? new HashMap<>(types) : new HashMap<>(); + } + + @Override + public boolean canProvide(Context context) { + return document != null && document.isJavaScript(); + } + + @Override + public List getSuggestions(Context context) { + List items = new ArrayList<>(); + + if (context.isMemberAccess) { + // Member access: resolve type of receiver and get its members + addMemberSuggestions(context, items); + } else { + // Identifier context: show variables, functions, types in scope + addScopeSuggestions(context, items); + } + + // Filter and score by prefix + filterAndScore(items, context.prefix); + + // Sort by score + Collections.sort(items); + + // Limit results + if (items.size() > 50) { + items = items.subList(0, 50); + } + + return items; + } + + /** + * Add suggestions for member access (after dot). + */ + private void addMemberSuggestions(Context context, List items) { + String receiverExpr = context.receiverExpression; + if (receiverExpr == null || receiverExpr.isEmpty()) { + return; + } + + // Infer type of receiver + String receiverType = inferExpressionType(receiverExpr); + if (receiverType == null) { + return; + } + + JSTypeInfo typeInfo = registry.getType(receiverType); + if (typeInfo == null) { + return; + } + + // Add methods + addMethodsFromType(typeInfo, items, new HashSet<>()); + + // Add fields + addFieldsFromType(typeInfo, items, new HashSet<>()); + } + + /** + * Recursively add methods from a type and its parents. + */ + private void addMethodsFromType(JSTypeInfo type, List items, Set added) { + for (JSMethodInfo method : type.getMethods().values()) { + String name = method.getName(); + // Skip overload markers (name$1, name$2, etc.) + if (name.contains("$")) { + name = name.substring(0, name.indexOf('$')); + } + if (!added.contains(name)) { + added.add(name); + items.add(AutocompleteItem.fromJSMethod(method)); + } + } + + // Add from parent type + if (type.getResolvedParent() != null) { + addMethodsFromType(type.getResolvedParent(), items, added); + } + } + + /** + * Recursively add fields from a type and its parents. + */ + private void addFieldsFromType(JSTypeInfo type, List items, Set added) { + for (JSFieldInfo field : type.getFields().values()) { + if (!added.contains(field.getName())) { + added.add(field.getName()); + items.add(AutocompleteItem.fromJSField(field)); + } + } + + // Add from parent type + if (type.getResolvedParent() != null) { + addFieldsFromType(type.getResolvedParent(), items, added); + } + } + + /** + * Add suggestions based on current scope (not after a dot). + */ + private void addScopeSuggestions(Context context, List items) { + // Add local variables + for (Map.Entry entry : variableTypes.entrySet()) { + String varName = entry.getKey(); + String varType = entry.getValue(); + + items.add(new AutocompleteItem.Builder() + .name(varName) + .insertText(varName) + .kind(AutocompleteItem.Kind.VARIABLE) + .typeLabel(varType != null ? varType : "any") + .build()); + } + + // Add hook functions + for (String hookName : registry.getHookNames()) { + List sigs = registry.getHookSignatures(hookName); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + items.add(new AutocompleteItem.Builder() + .name(hookName) + .insertText(hookName) + .kind(AutocompleteItem.Kind.METHOD) + .typeLabel("hook") + .signature("function " + hookName + "(e: " + sig.paramType + ")") + .documentation(sig.doc) + .build()); + } + } + + // Add global types + for (String typeName : registry.getTypeNames()) { + JSTypeInfo type = registry.getType(typeName); + if (type != null) { + items.add(new AutocompleteItem.Builder() + .name(typeName) + .insertText(typeName) + .kind(AutocompleteItem.Kind.CLASS) + .typeLabel("interface") + .build()); + } + } + + // Add JavaScript keywords + addJSKeywords(items); + } + + /** + * Infer the type of an expression. + */ + private String inferExpressionType(String expr) { + if (expr == null || expr.isEmpty()) { + return null; + } + + expr = expr.trim(); + + // Check if it's a known variable + if (variableTypes.containsKey(expr)) { + return variableTypes.get(expr); + } + + // Handle method chain: x.y.z() -> resolve step by step + if (expr.contains(".")) { + String[] parts = expr.split("\\."); + String currentType = null; + + // Start with the first part + if (variableTypes.containsKey(parts[0])) { + currentType = variableTypes.get(parts[0]); + } + + if (currentType == null) { + return null; + } + + // Walk through the chain + for (int i = 1; i < parts.length; i++) { + String member = parts[i]; + // Remove () for method calls + if (member.endsWith("()")) { + member = member.substring(0, member.length() - 2); + } + + JSTypeInfo typeInfo = registry.getType(currentType); + if (typeInfo == null) { + return null; + } + + // Check method + JSMethodInfo method = typeInfo.getMethod(member); + if (method != null) { + currentType = method.getReturnType(); + continue; + } + + // Check field + JSFieldInfo field = typeInfo.getField(member); + if (field != null) { + currentType = field.getType(); + continue; + } + + // Unknown member + return null; + } + + return currentType; + } + + return null; + } + + /** + * Add JavaScript keywords. + */ + private void addJSKeywords(List items) { + String[] keywords = { + "function", "var", "let", "const", "if", "else", "for", "while", "do", + "switch", "case", "break", "continue", "return", "try", "catch", "finally", + "throw", "new", "typeof", "instanceof", "in", "of", "this", "null", + "undefined", "true", "false", "async", "await", "yield", "class", "extends", + "import", "export", "default" + }; + + for (String keyword : keywords) { + items.add(AutocompleteItem.keyword(keyword)); + } + } + + /** + * Filter items by prefix and calculate match scores. + */ + private void filterAndScore(List items, String prefix) { + if (prefix == null || prefix.isEmpty()) { + for (AutocompleteItem item : items) { + item.calculateMatchScore(""); + } + return; + } + + Iterator iter = items.iterator(); + while (iter.hasNext()) { + AutocompleteItem item = iter.next(); + int score = item.calculateMatchScore(prefix); + if (score < 0) { + iter.remove(); + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java new file mode 100644 index 000000000..802482040 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -0,0 +1,287 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +/** + * Autocomplete provider for Java/Groovy scripts. + * Uses ScriptDocument for type resolution and scope analysis. + */ +public class JavaAutocompleteProvider implements AutocompleteProvider { + + private ScriptDocument document; + + public void setDocument(ScriptDocument document) { + this.document = document; + } + + @Override + public boolean canProvide(Context context) { + return document != null && !document.isJavaScript(); + } + + @Override + public List getSuggestions(Context context) { + List items = new ArrayList<>(); + + if (document == null) { + return items; + } + + if (context.isMemberAccess) { + // Member access: resolve type of receiver and get its members + addMemberSuggestions(context, items); + } else { + // Identifier context: show variables, methods, types in scope + addScopeSuggestions(context, items); + } + + // Filter and score by prefix + filterAndScore(items, context.prefix); + + // Sort by score + Collections.sort(items); + + // Limit results + if (items.size() > 50) { + items = items.subList(0, 50); + } + + return items; + } + + /** + * Add suggestions for member access (after dot). + */ + private void addMemberSuggestions(Context context, List items) { + String receiverExpr = context.receiverExpression; + if (receiverExpr == null || receiverExpr.isEmpty()) { + return; + } + + // Resolve the type of the receiver expression + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart); + + if (receiverType == null || !receiverType.isResolved()) { + return; + } + + Class clazz = receiverType.getJavaClass(); + if (clazz == null) { + // Try ScriptTypeInfo + if (receiverType instanceof ScriptTypeInfo) { + addScriptTypeMembers((ScriptTypeInfo) receiverType, items); + } + return; + } + + // Add methods + Set addedMethods = new HashSet<>(); + for (Method method : clazz.getMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + String sig = method.getName() + "(" + method.getParameterCount() + ")"; + if (!addedMethods.contains(sig)) { + addedMethods.add(sig); + MethodInfo methodInfo = MethodInfo.fromReflection(method, receiverType); + items.add(AutocompleteItem.fromMethod(methodInfo)); + } + } + } + + // Add fields + for (Field field : clazz.getFields()) { + if (Modifier.isPublic(field.getModifiers())) { + FieldInfo fieldInfo = FieldInfo.fromReflection(field, receiverType); + items.add(AutocompleteItem.fromField(fieldInfo)); + } + } + + // Add enum constants if it's an enum + if (clazz.isEnum()) { + for (Object constant : clazz.getEnumConstants()) { + String constantName = constant.toString(); + items.add(new AutocompleteItem.Builder() + .name(constantName) + .insertText(constantName) + .kind(AutocompleteItem.Kind.ENUM_CONSTANT) + .typeLabel(receiverType.getSimpleName()) + .build()); + } + } + } + + /** + * Add members from a script-defined type. + */ + private void addScriptTypeMembers(ScriptTypeInfo scriptType, List items) { + // Add methods (getMethods returns Map>) + for (List overloads : scriptType.getMethods().values()) { + for (MethodInfo method : overloads) { + items.add(AutocompleteItem.fromMethod(method)); + } + } + + // Add fields (getFields returns Map) + for (FieldInfo field : scriptType.getFields().values()) { + items.add(AutocompleteItem.fromField(field)); + } + + // Add parent class members + if (scriptType.hasSuperClass()) { + TypeInfo superType = scriptType.getSuperClass(); + if (superType != null && superType.getJavaClass() != null) { + Class superClazz = superType.getJavaClass(); + Set addedMethods = new HashSet<>(); + for (Method method : superClazz.getMethods()) { + if (Modifier.isPublic(method.getModifiers())) { + String sig = method.getName() + "(" + method.getParameterCount() + ")"; + if (!addedMethods.contains(sig)) { + addedMethods.add(sig); + MethodInfo methodInfo = MethodInfo.fromReflection(method, superType); + items.add(AutocompleteItem.fromMethod(methodInfo)); + } + } + } + } + } + } + + /** + * Add suggestions based on current scope (not after a dot). + */ + private void addScopeSuggestions(Context context, List items) { + int pos = context.cursorPosition; + + // Find containing method + MethodInfo containingMethod = document.findContainingMethod(pos); + + // Add local variables + if (containingMethod != null) { + Map locals = document.getLocalsForMethod(containingMethod); + if (locals != null) { + for (FieldInfo local : locals.values()) { + if (local.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(local)); + } + } + } + + // Add method parameters + for (FieldInfo param : containingMethod.getParameters()) { + items.add(AutocompleteItem.fromField(param)); + } + } + + // Add global fields + for (FieldInfo globalField : document.getGlobalFields().values()) { + if (globalField.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(globalField)); + } + } + + // Add enclosing type fields (find via script types map) + ScriptTypeInfo enclosingType = findEnclosingType(pos); + if (enclosingType != null) { + for (FieldInfo field : enclosingType.getFields().values()) { + if (field.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(field)); + } + } + + // Add methods from enclosing type + for (List overloads : enclosingType.getMethods().values()) { + for (MethodInfo method : overloads) { + items.add(AutocompleteItem.fromMethod(method)); + } + } + } + + // Add script-defined methods + for (MethodInfo method : document.getAllMethods()) { + items.add(AutocompleteItem.fromMethod(method)); + } + + // Add imported types + for (TypeInfo type : document.getImportedTypes()) { + items.add(AutocompleteItem.fromType(type)); + } + + // Add script-defined types + for (ScriptTypeInfo scriptType : document.getScriptTypesMap().values()) { + items.add(AutocompleteItem.fromType(scriptType)); + } + + // Add keywords + addKeywords(items); + } + + /** + * Add Java keywords. + */ + private void addKeywords(List items) { + String[] keywords = { + "if", "else", "for", "while", "do", "switch", "case", "break", "continue", + "return", "try", "catch", "finally", "throw", "throws", "new", "this", "super", + "true", "false", "null", "instanceof", "import", "class", "interface", "enum", + "extends", "implements", "public", "private", "protected", "static", "final", + "abstract", "synchronized", "volatile", "transient", "native", "void", + "boolean", "byte", "short", "int", "long", "float", "double", "char" + }; + + for (String keyword : keywords) { + items.add(AutocompleteItem.keyword(keyword)); + } + } + + /** + * Find the enclosing script type at a position. + * This is a workaround since findEnclosingScriptType is package-private. + */ + private ScriptTypeInfo findEnclosingType(int position) { + for (ScriptTypeInfo type : document.getScriptTypesMap().values()) { + if (type.containsPosition(position)) { + return type; + } + } + return null; + } + + /** + * Filter items by prefix and calculate match scores. + */ + private void filterAndScore(List items, String prefix) { + if (prefix == null || prefix.isEmpty()) { + // No filtering needed, all items get a base score + for (AutocompleteItem item : items) { + item.calculateMatchScore(""); + } + return; + } + + // Filter and score + Iterator iter = items.iterator(); + while (iter.hasNext()) { + AutocompleteItem item = iter.next(); + int score = item.calculateMatchScore(prefix); + if (score < 0) { + iter.remove(); + } + } + } + + /** + * Builder class for AutocompleteItem to handle cases without source data. + */ + private static class AutocompleteItemBuilder { + // This would be needed if AutocompleteItem had a builder pattern + } +} 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 fdf540934..456e08e02 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 @@ -161,7 +161,7 @@ public String getLanguage() { * Check if this is a JavaScript/ECMAScript document. */ public boolean isJavaScript() { - return "ECMAScript".equalsIgnoreCase(language); + return false;//"ECMAScript".equalsIgnoreCase(language); } // ==================== TEXT MANAGEMENT ==================== @@ -2592,7 +2592,7 @@ private boolean hasExcessivePrecision(String numLiteral, int maxDigits) { * * This is THE method that should be used for all type resolution needs. */ - TypeInfo resolveExpressionType(String expr, int position) { + public TypeInfo resolveExpressionType(String expr, int position) { expr = expr.trim(); if (expr.isEmpty()) { @@ -5183,4 +5183,47 @@ private TypeInfo lookupVariableType(String varName, int position) { return null; } + + // ==================== AUTOCOMPLETE SUPPORT ==================== + + /** + * Find the method that contains the given position. + * Public accessor for autocomplete. + */ + public MethodInfo findContainingMethod(int position) { + return findMethodAtPosition(position); + } + + /** + * Get local variables for a specific method. + * Used by autocomplete to show variables in scope. + */ + public Map getLocalsForMethod(MethodInfo method) { + if (method == null) return null; + return methodLocals.get(method.getDeclarationOffset()); + } + + /** + * Get all imported types that have been resolved. + * Used by autocomplete to show available types. + */ + public List getImportedTypes() { + List types = new ArrayList<>(); + for (ImportData imp : imports) { + if (!imp.isWildcard() && imp.isResolved()) { + TypeInfo type = typeResolver.resolveSimpleName(imp.getSimpleName(), importsBySimpleName, wildcardPackages); + if (type != null && type.isResolved()) { + types.add(type); + } + } + } + return types; + } + + /** + * Get script types as a Map (needed by autocomplete). + */ + public Map getScriptTypesMap() { + return Collections.unmodifiableMap(scriptTypes); + } } 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 7234197d1..bc6b71688 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 @@ -335,6 +335,20 @@ public Collection getAllTypes() { return types.values(); } + /** + * Get all type names. + */ + public Set getTypeNames() { + return types.keySet(); + } + + /** + * Get all hook function names. + */ + public Set getHookNames() { + return hooks.keySet(); + } + /** * Get all registered hooks. */ @@ -365,10 +379,16 @@ public void clear() { public static class HookSignature { public final String paramName; public final String paramType; + public final String doc; public HookSignature(String paramName, String paramType) { + this(paramName, paramType, null); + } + + public HookSignature(String paramName, String paramType, String doc) { this.paramName = paramName; this.paramType = paramType; + this.doc = doc; } @Override diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index f8f262893..8815cd55a 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -30,6 +30,9 @@ public class ScriptEditorKeys extends KeyPresetManager { // View public final KeyPreset FULLSCREEN = add("Toggle Fullscreen").setDefaultState(Keyboard.KEY_F11, false, false, false); + + // Autocomplete + public final KeyPreset AUTOCOMPLETE = add("Autocomplete").setDefaultState(Keyboard.KEY_SPACE, true, false, false); public ScriptEditorKeys() { super("script_editor"); From c4d5178472c4119aaf64789644588c6efe3549e4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 14:48:32 +0200 Subject: [PATCH 162/337] Moved KeyPrest isDown handling to KeyState --- .../noppes/npcs/client/key/KeyPreset.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 7bae2c58c..03b586a72 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -92,10 +92,8 @@ public void tick() { int keyCode = keyCode(); if (keyCode == -1 || keyCode == 0) return; - - boolean isDown = isMouseKey() ? Mouse.isButtonDown(keyCode + 100) : Keyboard.isKeyDown(keyCode); - isDown = isDown && (isCtrlKeyDown() == hasCtrl()) && (isAltKeyDown() == hasAlt()) && (isShiftKeyDown() == hasShift()); - setDown(isDown); + + setDown(currentState.isDown()); } private static final long SHORT_PRESS_MS = 250L; @@ -209,6 +207,11 @@ public void writeTo(KeyState state) { state.setState(keyCode, hasCtrl, hasAlt, hasShift); } + public boolean isDown() { + boolean isDown = isMouseKey() ? Mouse.isButtonDown(keyCode + 100) : Keyboard.isKeyDown(keyCode); + return isDown && (isCtrlKeyDown() == hasCtrl) && (isAltKeyDown() == hasAlt) && (isShiftKeyDown() == hasShift); + } + public boolean hasState() { return keyCode != -1; } @@ -246,6 +249,19 @@ public void readFromNbt(NBTTagCompound compound) { this.hasAlt = compound.getBoolean("hasAlt"); } + public boolean matches(int keycode, boolean checkModifiers) { + if (!checkModifiers) + return this.keyCode == keycode; + + return this.keyCode == keycode && this.hasCtrl == KeyPreset.isCtrlKeyDown() + && this.hasAlt == KeyPreset.isAltKeyDown() + && this.hasShift == KeyPreset.isShiftKeyDown(); + } + + public boolean isMouseKey() { + return keyCode < -1; + } + public String getName() { int code = keyCode; String name = ""; From 85d79dd786dcc35d10063f6572154d26f4b6138a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 3 Jan 2026 14:49:05 +0200 Subject: [PATCH 163/337] Disabled script area textbox typing when a matching keycode KeyPreset is down --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 10 +++++++--- .../java/noppes/npcs/client/key/KeyPresetManager.java | 9 +++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) 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 32aac7680..266c8809a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -4,7 +4,6 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; -import net.minecraft.util.ResourceLocation; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.script.GuiScriptInterface; @@ -1108,8 +1107,8 @@ private Object[] getTokenAtScreenPosition(int xMouse, int yMouse) { */ private void updateHoverState(int xMouse, int yMouse) { // Don't show tooltips when not active, clicking, or when overlays are visible - if (!isEnabled() || clicked || searchBar.isVisible() || - goToLineDialog.isVisible() || KEYS_OVERLAY.isVisible() || renameHandler.isActive()) { + if (!isEnabled() || clicked || searchBar.isVisible() || + goToLineDialog.isVisible() || KEYS_OVERLAY.isVisible() || renameHandler.isActive() || autocompleteManager.isVisible()) { hoverState.clearHover(); return; } @@ -1434,6 +1433,11 @@ public boolean textboxKeyTyped(char c, int i) { } } + // Ignore if any global keys bound to this code are currently pressed + if (KEYS.hasMatchingKeyPressed(i)) + return false; + + if (!active) return false; diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java index 6f973e2b5..7327a6e4a 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -24,6 +24,15 @@ public KeyPreset add(String name) { return preset; } + public boolean hasMatchingKeyPressed(int keyCode) { + for (KeyPreset key : keys) { + if (key.currentState.matches(keyCode, true)) + return true; + } + + return false; + } + public void tick() { for (KeyPreset key : keys) key.tick(); From ea38cd23b64beb776695e08aa583c0bd4d1bc325 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 00:23:47 +0200 Subject: [PATCH 164/337] Rendered Script Text Areas last thing in GuiNpcInterface --- .../java/noppes/npcs/client/gui/util/GuiNPCInterface.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 37dce6f79..4516d5606 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -377,9 +377,6 @@ public void drawScreen(int i, int j, float f) { drawCenteredString(fontRendererObj, title, width / 2, guiTop + 4, 0xffffff); for (GuiNpcLabel label : labels.values()) label.drawLabel(this, fontRendererObj); - for (GuiNpcTextField tf : textfields.values()) { - tf.drawTextBox(i, j); - } for (GuiCustomScroll scroll : scrolls.values()) { scroll.updateSubGUI(subGui); scroll.drawScreen(i, j, f, !subGui && scroll.isMouseOver(i, j) ? Mouse.getDWheel() : 0); @@ -402,7 +399,10 @@ public void drawScreen(int i, int j, float f) { button.drawHover(i, j, subGui); } } - + for (GuiNpcTextField tf : textfields.values()) { + tf.drawTextBox(i, j); + } + for (GuiScreen gui : extra.values()) gui.drawScreen(i, j, f); From 117f85096f8a208f193ebce8187d813a86539c93 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 00:40:46 +0200 Subject: [PATCH 165/337] Added mouse drag to autocomplete scroll bar --- .../client/gui/util/GuiScriptTextArea.java | 4 +- .../script/autocomplete/AutocompleteMenu.java | 66 ++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) 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 266c8809a..bdf7d72be 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -961,10 +961,10 @@ public void drawTextBox(int xMouse, int yMouse) { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); + // Draw autocomplete menu (overlays code area) autocompleteManager.draw(xMouse, yMouse); - - KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); // Draw hover tooltips (on top of everything) if (hoverState.isTooltipVisible()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index ea56e7deb..95ba9bebe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -42,6 +42,11 @@ public class AutocompleteMenu extends Gui { private int selectedIndex = 0; private int scrollOffset = 0; private int hoveredIndex = -1; + + // Scrollbar drag state + private boolean isDraggingScrollbar = false; + private int dragStartY = 0; + private int dragStartScroll = 0; // ==================== POSITION ==================== private int x, y; @@ -266,7 +271,7 @@ public void draw(int mouseX, int mouseY) { // Draw scrollbar if needed if (items.size() > MAX_VISIBLE_ITEMS) { - drawScrollbar(); + drawScrollbar(mouseX, mouseY); } // Draw hint bar at bottom @@ -337,7 +342,7 @@ private void drawHighlightedText(String text, int[] matchIndices, int x, int y, /** * Draw the scrollbar. */ - private void drawScrollbar() { + private void drawScrollbar(int mouseX, int mouseY) { int scrollbarX = x + width - 8; int scrollbarY = y + PADDING; int scrollbarHeight = MAX_VISIBLE_ITEMS * ITEM_HEIGHT; @@ -351,7 +356,13 @@ private void drawScrollbar() { float thumbPosRatio = (float) scrollOffset / Math.max(1, items.size() - MAX_VISIBLE_ITEMS); int thumbY = scrollbarY + (int) ((scrollbarHeight - thumbHeight) * thumbPosRatio); - drawRect(scrollbarX + 1, thumbY, scrollbarX + 5, thumbY + thumbHeight, SCROLLBAR_FG); + // Check if mouse is above the scrollbar thumb + boolean isAboveScrollbar = mouseX >= scrollbarX && mouseX <= scrollbarX + 6 && + mouseY >= thumbY && mouseY < thumbY + thumbHeight; + + int col = isDraggingScrollbar || isAboveScrollbar? 0xFF808080 : SCROLLBAR_FG; + + drawRect(scrollbarX + 1, thumbY, scrollbarX + 5, thumbY + thumbHeight, col); } /** @@ -429,6 +440,18 @@ public boolean mouseClicked(int mouseX, int mouseY, int button) { hide(); return false; } + + // Check if clicking on scrollbar + if (button == 0 && items.size() > MAX_VISIBLE_ITEMS) { + int scrollbarX = x + width - 8; + if (mouseX >= scrollbarX && mouseX <= scrollbarX + 6) { + // Clicked on scrollbar area - start drag + isDraggingScrollbar = true; + dragStartY = mouseY; + dragStartScroll = scrollOffset; + return true; + } + } // Check if clicking on an item if (button == 0 && hoveredIndex >= 0 && hoveredIndex < items.size()) { @@ -439,6 +462,43 @@ public boolean mouseClicked(int mouseX, int mouseY, int button) { return true; // Consume click if inside menu } + + /** + * Handle mouse release. + * @return true if release was consumed + */ + public boolean mouseReleased(int mouseX, int mouseY, int button) { + if (button == 0 && isDraggingScrollbar) { + isDraggingScrollbar = false; + return true; + } + return false; + } + + /** + * Handle mouse drag. + * @return true if drag was consumed + */ + public boolean mouseDragged(int mouseX, int mouseY) { + if (!visible || !isDraggingScrollbar) + return false; + + // Calculate scroll area height + int listHeight = MAX_VISIBLE_ITEMS * ITEM_HEIGHT; + int scrollbarHeight = Math.max(20, (listHeight * MAX_VISIBLE_ITEMS) / items.size()); + int scrollTrackHeight = listHeight - scrollbarHeight; + + // Calculate new scroll offset based on drag + int deltaY = mouseY - dragStartY; + int maxScroll = items.size() - MAX_VISIBLE_ITEMS; + + if (scrollTrackHeight > 0) { + int scrollDelta = (deltaY * maxScroll) / scrollTrackHeight; + scrollOffset = Math.max(0, Math.min(maxScroll, dragStartScroll + scrollDelta)); + } + + return true; + } /** * Handle mouse scroll. From d1db2067f85beeb5abc3cfdf3e1806860952c0d4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 00:41:33 +0200 Subject: [PATCH 166/337] oops forgor --- .../autocomplete/AutocompleteManager.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 0af1a3024..fc54738ac 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -3,6 +3,7 @@ import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import org.lwjgl.input.Mouse; import java.util.ArrayList; import java.util.List; @@ -177,6 +178,50 @@ public void onDeleteKey(String text, int cursorPosition) { } } } + + /** + * Called when cursor position changes (e.g., arrow keys). + * Updates the autocomplete prefix based on the new cursor position. + */ + public void onCursorMove(String text, int cursorPosition) { + if (!active) + return; + + // Check if we're still in a valid identifier position + if (cursorPosition < 0 || cursorPosition > text.length()) { + dismiss(); + return; + } + + // Find the word boundaries at the current position + int wordStart = cursorPosition; + while (wordStart > 0 && Character.isJavaIdentifierPart(text.charAt(wordStart - 1))) { + wordStart--; + } + + int wordEnd = cursorPosition; + while (wordEnd < text.length() && Character.isJavaIdentifierPart(text.charAt(wordEnd))) { + wordEnd++; + } + + // Check if we moved out of the current word + if (cursorPosition < prefixStartPosition || cursorPosition > prefixStartPosition + currentPrefix.length()) { + // Check if we're in a different word that we can autocomplete + boolean isMemberAccess = wordStart > 0 && text.charAt(wordStart - 1) == '.'; + + if (isMemberAccess || wordStart < cursorPosition) { + // Update to the new word + prefixStartPosition = wordStart; + updatePrefix(text, cursorPosition); + } else { + // Moved to empty space or invalid position + dismiss(); + } + } else { + // Still in same word, just update the prefix + updatePrefix(text, cursorPosition); + } + } /** * Explicitly trigger autocomplete (Ctrl+Space). @@ -373,6 +418,26 @@ public boolean mouseScrolled(int mouseX, int mouseY, int delta) { if (!active) return false; return menu.mouseScrolled(mouseX, mouseY, delta); } + + /** + * Handle mouse release. + * @return true if the release was consumed + */ + public boolean mouseReleased(int mouseX, int mouseY, int button) { + if (!active) + return false; + return menu.mouseReleased(mouseX, mouseY, button); + } + + /** + * Handle mouse drag. + * @return true if the drag was consumed + */ + public boolean mouseDragged(int mouseX, int mouseY) { + if (!active) + return false; + return menu.mouseDragged(mouseX, mouseY); + } // ==================== ITEM SELECTION ==================== @@ -416,6 +481,11 @@ public void dismiss() { public void draw(int mouseX, int mouseY) { if (active) { menu.draw(mouseX, mouseY); + + if (Mouse.isButtonDown(0)) + mouseDragged(mouseX, mouseY); + else + mouseReleased(mouseX, mouseY, 0); } } From 82632d9131152bd4ef8cf05765216e7024e9d700 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 00:42:00 +0200 Subject: [PATCH 167/337] Made Autocomplete respect static/non static contexts --- .../JavaAutocompleteProvider.java | 35 +++++++++++++++++-- .../script/interpreter/ScriptDocument.java | 2 +- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 802482040..90dd15b53 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -75,11 +75,14 @@ private void addMemberSuggestions(Context context, List items) return; } + // Determine if this is a static context (accessing a class type) + boolean isStaticContext = isStaticAccess(receiverExpr, context.prefixStart); + Class clazz = receiverType.getJavaClass(); if (clazz == null) { // Try ScriptTypeInfo if (receiverType instanceof ScriptTypeInfo) { - addScriptTypeMembers((ScriptTypeInfo) receiverType, items); + addScriptTypeMembers((ScriptTypeInfo) receiverType, items, isStaticContext); } return; } @@ -88,6 +91,11 @@ private void addMemberSuggestions(Context context, List items) Set addedMethods = new HashSet<>(); for (Method method : clazz.getMethods()) { if (Modifier.isPublic(method.getModifiers())) { + // Filter by static context + if (isStaticContext && !Modifier.isStatic(method.getModifiers())) { + continue; + } + String sig = method.getName() + "(" + method.getParameterCount() + ")"; if (!addedMethods.contains(sig)) { addedMethods.add(sig); @@ -100,6 +108,11 @@ private void addMemberSuggestions(Context context, List items) // Add fields for (Field field : clazz.getFields()) { if (Modifier.isPublic(field.getModifiers())) { + // Filter by static context + if (isStaticContext && !Modifier.isStatic(field.getModifiers())) { + continue; + } + FieldInfo fieldInfo = FieldInfo.fromReflection(field, receiverType); items.add(AutocompleteItem.fromField(fieldInfo)); } @@ -122,16 +135,24 @@ private void addMemberSuggestions(Context context, List items) /** * Add members from a script-defined type. */ - private void addScriptTypeMembers(ScriptTypeInfo scriptType, List items) { + private void addScriptTypeMembers(ScriptTypeInfo scriptType, List items, boolean isStaticContext) { // Add methods (getMethods returns Map>) for (List overloads : scriptType.getMethods().values()) { for (MethodInfo method : overloads) { + // Filter by static context + if (isStaticContext && !method.isStatic()) { + continue; + } items.add(AutocompleteItem.fromMethod(method)); } } // Add fields (getFields returns Map) for (FieldInfo field : scriptType.getFields().values()) { + // Filter by static context + if (isStaticContext && !field.isStatic()) { + continue; + } items.add(AutocompleteItem.fromField(field)); } @@ -255,6 +276,16 @@ private ScriptTypeInfo findEnclosingType(int position) { return null; } + /** + * Check if the receiver expression represents static access (class type). + * Similar logic to FieldChainMarker.isStaticContext(). + */ + private boolean isStaticAccess(String receiverExpr, int position) { + // Try to resolve the receiver expression as a type + TypeInfo typeCheck = document.resolveType(receiverExpr); + return typeCheck != null && typeCheck.isResolved(); + } + /** * Filter items by prefix and calculate match scores. */ 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 456e08e02..ca037bdc8 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 @@ -1439,7 +1439,7 @@ private String cleanDocumentation(String comment) { return result.isEmpty() ? null : result; } - TypeInfo resolveType(String typeName) { + public TypeInfo resolveType(String typeName) { return resolveTypeAndTrackUsage(typeName); } From 516304449427f2aae1e5a1f1f5dd4bb5185bf3fd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 00:55:50 +0200 Subject: [PATCH 168/337] Added args to autocomplete methods and highlighted them in dark gray --- .../script/autocomplete/AutocompleteItem.java | 23 +++++++++++-- .../script/autocomplete/AutocompleteMenu.java | 33 +++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 79140da21..b5ddc7ae5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -82,8 +82,23 @@ public static AutocompleteItem fromMethod(MethodInfo method) { String signature = buildMethodSignature(method); + // Build display name with parameters like "read(byte[] b)" + StringBuilder displayName = new StringBuilder(name); + displayName.append("("); + for (int i = 0; i < method.getParameterCount(); i++) { + if (i > 0) displayName.append(", "); + FieldInfo param = method.getParameters().get(i); + String paramType = param.getTypeInfo() != null ? + param.getTypeInfo().getSimpleName() : "?"; + displayName.append(paramType); + if (param.getName() != null && !param.getName().isEmpty()) { + displayName.append(" ").append(param.getName()); + } + } + displayName.append(")"); + return new AutocompleteItem( - name, + displayName.toString(), insertText.toString(), Kind.METHOD, returnType, @@ -165,8 +180,12 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { insertText.append("("); insertText.append(")"); + // Use signature for display name if available, otherwise just name + String displayName = method.getSignature() != null && method.getSignature().contains("(") ? + method.getSignature().substring(method.getSignature().indexOf(name)) : name + "()"; + return new AutocompleteItem( - name, + displayName, insertText.toString(), Kind.METHOD, method.getReturnType(), diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index 95ba9bebe..b73ac42a3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -301,9 +301,14 @@ private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth font.drawString(icon, textX + (ICON_WIDTH - font.getStringWidth(icon)) / 2, textY, iconColor); textX += ICON_WIDTH; - // Draw name with match highlighting - drawHighlightedText(item.getName(), item.getMatchIndices(), textX, textY, - item.isDeprecated() ? DIM_TEXT_COLOR : TEXT_COLOR); + // Draw name with match highlighting and parameter coloring for methods + if (item.getKind() == AutocompleteItem.Kind.METHOD) { + drawMethodName(item.getName(), item.getMatchIndices(), textX, textY, + item.isDeprecated() ? DIM_TEXT_COLOR : TEXT_COLOR); + } else { + drawHighlightedText(item.getName(), item.getMatchIndices(), textX, textY, + item.isDeprecated() ? DIM_TEXT_COLOR : TEXT_COLOR); + } // Draw type label on the right if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) { @@ -314,6 +319,28 @@ private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth } } + /** + * Draw method name with parameters colored gray. + */ + private void drawMethodName(String text, int[] matchIndices, int x, int y, int baseColor) { + // Find the opening parenthesis + int parenIndex = text.indexOf('('); + if (parenIndex == -1) { + // No parameters, just draw normally + drawHighlightedText(text, matchIndices, x, y, baseColor); + return; + } + + // Draw method name part with highlighting + String methodName = text.substring(0, parenIndex); + drawHighlightedText(methodName, matchIndices, x, y, baseColor); + + // Draw parameters part in gray (no highlighting) + String params = text.substring(parenIndex); + int paramX = x + font.getStringWidth(methodName); + font.drawString(params, paramX, y, DIM_TEXT_COLOR); + } + /** * Draw text with specific characters highlighted. */ From b42cd4d1a3dec6130605746d5adc4dde4264c9b4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 01:02:47 +0200 Subject: [PATCH 169/337] Dismissed AutoCompleteManager on gui init/window resize --- .../client/gui/util/GuiScriptTextArea.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 bdf7d72be..c06b21459 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -163,6 +163,11 @@ public void initGui() { KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); + // Dismiss autocomplete on resize to avoid positioning issues + if (autocompleteManager != null) { + autocompleteManager.dismiss(); + } + // Initialize search bar (preserves state across initGui calls) searchBar.initGui(x, y, width); if (searchBar.isVisible()) { // If open @@ -592,7 +597,7 @@ public void drawTextBox(int xMouse, int yMouse) { doubleClicked = false; tripleClicked = false; } - setCursor(i, true); + setCursor(i, true); } } else if (doubleClicked || tripleClicked) { doubleClicked = false; @@ -1484,6 +1489,12 @@ private boolean handleNavigationKeys(int i) { int newPos = Math.max(selection.getCursorPosition() - j, 0); // If Shift is held, extend selection; otherwise place caret. setCursor(newPos, GuiScreen.isShiftKeyDown()); + + // Notify autocomplete of cursor movement + if (autocompleteManager.isVisible()) { + autocompleteManager.onCursorMove(text, newPos); + } + return true; } @@ -1509,6 +1520,12 @@ private boolean handleNavigationKeys(int i) { } int newPos = Math.min(selection.getCursorPosition() + j, text.length()); setCursor(newPos, GuiScreen.isShiftKeyDown()); + + // Notify autocomplete of cursor movement + if (autocompleteManager.isVisible()) { + autocompleteManager.onCursorMove(text, newPos); + } + return true; } From 297c8129debe710495c291e5cd4d44e72cc75bd5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 01:09:55 +0200 Subject: [PATCH 170/337] Autocomplete ignores whitespaces between cursor and receiver "Minecraft.getMinecraft(). " --- .../autocomplete/AutocompleteManager.java | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index fc54738ac..6eea5a9f5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -153,8 +153,15 @@ public void onCharTyped(char c, String text, int cursorPosition) { // Update existing autocomplete updatePrefix(text, cursorPosition); } else if (Character.isJavaIdentifierStart(c)) { - // Potentially start new autocomplete - maybeStartAutocomplete(text, cursorPosition, false); + // Check if we're after a dot with whitespace (e.g., "obj. |" where | is cursor) + int dotPos = findDotBeforeWhitespace(text, cursorPosition - 1); + if (dotPos >= 0) { + // We're typing after a dot (with possible whitespace), trigger member access + triggerAfterDot(text, cursorPosition); + } else { + // Potentially start new autocomplete + maybeStartAutocomplete(text, cursorPosition, false); + } } return; } @@ -247,9 +254,11 @@ public void triggerExplicit() { */ private void triggerAfterDot(String text, int cursorPosition) { // Find the receiver expression before the dot + // First check if immediately before cursor int dotPos = cursorPosition - 1; if (dotPos < 0 || text.charAt(dotPos) != '.') { - dotPos = text.lastIndexOf('.', cursorPosition - 1); + // Look backwards skipping whitespace to find dot + dotPos = findDotBeforeWhitespace(text, cursorPosition - 1); } if (dotPos < 0) return; @@ -257,7 +266,13 @@ private void triggerAfterDot(String text, int cursorPosition) { String receiverExpr = findReceiverExpression(text, dotPos); String prefix = findCurrentWord(text, cursorPosition); - prefixStartPosition = dotPos + 1; + // Find where the prefix actually starts (after dot + any whitespace) + int prefixStart = dotPos + 1; + while (prefixStart < cursorPosition && Character.isWhitespace(text.charAt(prefixStart))) { + prefixStart++; + } + + prefixStartPosition = prefixStart; currentPrefix = prefix; showSuggestions(text, cursorPosition, prefix, prefixStartPosition, true, receiverExpr); @@ -306,13 +321,13 @@ private void updatePrefix(String text, int cursorPosition) { currentPrefix = newPrefix; - // Check if after dot - boolean isMemberAccess = prefixStartPosition > 0 && - text.charAt(prefixStartPosition - 1) == '.'; + // Check if after dot (skipping whitespace) + int dotPos = findDotBeforeWhitespace(text, prefixStartPosition - 1); + boolean isMemberAccess = dotPos >= 0; String receiverExpr = null; if (isMemberAccess) { - receiverExpr = findReceiverExpression(text, prefixStartPosition - 1); + receiverExpr = findReceiverExpression(text, dotPos); } showSuggestions(text, cursorPosition, currentPrefix, prefixStartPosition, @@ -576,6 +591,23 @@ private String findReceiverExpression(String text, int dotPos) { return expr; } + /** + * Find dot position before cursor, skipping whitespace. + * Returns -1 if no dot found. + */ + private int findDotBeforeWhitespace(String text, int fromPos) { + int pos = fromPos; + // Skip backwards over whitespace + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + // Check if we found a dot + if (pos >= 0 && text.charAt(pos) == '.') { + return pos; + } + return -1; + } + /** * Check if cursor is after a dot. */ @@ -585,6 +617,10 @@ private boolean isAfterDot(String text, int cursorPos) { while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) { pos--; } + // Also skip whitespace to check for dot + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } return pos >= 0 && text.charAt(pos) == '.'; } From bf858a7a1af80f08e77b06e22fbb9bf72183b82d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 01:27:14 +0200 Subject: [PATCH 171/337] Removed AutoComplete list limit --- .../util/script/autocomplete/AutocompleteManager.java | 10 ++++++++++ .../script/autocomplete/JavaAutocompleteProvider.java | 5 ----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 6eea5a9f5..bc725806f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -25,6 +25,9 @@ public class AutocompleteManager { /** Minimum characters before showing suggestions (for non-dot triggers) */ private static final int MIN_PREFIX_LENGTH = 1; + + /** Maximum number of suggestions to show (performance/UX) */ + private static final int MAX_SUGGESTIONS = 150; /** Pattern for identifier characters */ private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*"); @@ -356,6 +359,13 @@ private void showSuggestions(String text, int cursorPosition, String prefix, // Get suggestions from appropriate provider AutocompleteProvider provider = document.isJavaScript() ? jsProvider : javaProvider; List suggestions = provider.getSuggestions(context); + + // Limit suggestions to prevent overwhelming the UI and improve performance + // Items are already sorted by relevance in the provider + if (suggestions.size() > MAX_SUGGESTIONS) { + // Disabled for now to show all suggestions + // suggestions = suggestions.subList(0, MAX_SUGGESTIONS); + } // Show menu if (suggestions.isEmpty()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 90dd15b53..d3b64d877 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -51,11 +51,6 @@ public List getSuggestions(Context context) { // Sort by score Collections.sort(items); - // Limit results - if (items.size() > 50) { - items = items.subList(0, 50); - } - return items; } From a4112008bde4af4f90518064491a603ff2a1fb3c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 01:49:01 +0200 Subject: [PATCH 172/337] OOps wrong logic --- .../java/noppes/npcs/client/gui/script/GuiScriptInterface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6a7d201b3..4d588f651 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -384,7 +384,7 @@ public void confirmClicked(boolean flag, int i) { private GuiScriptTextArea getActiveScriptArea() { if (this.activeTab > 0) { int idx = this.activeTab - 1; - if (idx >= 0 && idx < textAreas.size()) + if (textAreas.containsKey(idx)) return textAreas.get(idx); } return null; From ef51f4acd8d4205d85f4f799e9a5dc1398db89e8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 01:56:13 +0200 Subject: [PATCH 173/337] Fixed buttons drawn under AutoComplete drop down being clickable through it --- .../client/gui/script/GuiScriptInterface.java | 12 ++++++++- .../client/gui/util/GuiScriptTextArea.java | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) 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 4d588f651..ec2acb90d 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -279,10 +279,20 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { // ==================== MOUSE HANDLING ==================== + @Override @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check if click is within autocomplete menu bounds and consume it if so + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea != null && activeArea.isClickInAutocompleteMenu(mouseX, mouseY)) { + activeArea.mouseClicked(mouseX, mouseY, mouseButton); + return; + } + // Check fullscreen button first when on script editor tab - if (this.activeTab > 0 && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + // BUT only if autocomplete is not visible (don't let clicks pass through autocomplete menu) + if (this.activeTab > 0 && (activeArea == null || !activeArea.isAutocompleteVisible()) + && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { return; } 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 c06b21459..5f6aa0653 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -11,6 +11,7 @@ import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; // New interpreter system imports +import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteMenu; import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; @@ -2489,6 +2490,32 @@ private void clampSelectionBounds() { selection.clamp(text != null ? text.length() : 0); } + // ==================== AUTOCOMPLETE VISIBILITY ==================== + + /** + * Check if a click position is within the bounds of the autocomplete menu. + * Returns false if autocomplete is not visible. + */ + public boolean isClickInAutocompleteMenu(int mouseX, int mouseY) { + if (autocompleteManager == null || !autocompleteManager.isVisible()) { + return false; + } + + + AutocompleteMenu menu = autocompleteManager.getMenu(); + if (menu == null) { + return false; + } + + int menuX = menu.getX(); + int menuY = menu.getY(); + int menuWidth = menu.getWidth(); + int menuHeight = menu.getHeight(); + + return mouseX >= menuX && mouseX <= menuX + menuWidth && + mouseY >= menuY && mouseY <= menuY + menuHeight; + } + // ==================== INNER CLASSES ==================== public static class UndoData { From 9886dae519e8eee99c3ed0ccfc5f63aff00665f2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 02:13:33 +0200 Subject: [PATCH 174/337] Moved fullScreenButton rendering into GuiScriptTextArea --- .../client/gui/script/GuiScriptInterface.java | 16 +++++----------- .../npcs/client/gui/util/GuiScriptTextArea.java | 10 +++++++++- 2 files changed, 14 insertions(+), 12 deletions(-) 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 ec2acb90d..ed1a54934 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -6,7 +6,6 @@ import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; @@ -30,7 +29,6 @@ import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; -import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.Date; @@ -71,7 +69,7 @@ public static class FullscreenConfig { } /** Fullscreen toggle button drawn at top-right of viewport */ - private final FullscreenButton fullscreenButton = new FullscreenButton(); + public final FullscreenButton fullscreenButton = new FullscreenButton(); public GuiScriptInterface() { this.drawDefaultBackground = true; @@ -270,28 +268,24 @@ private void initSettingsTab(int yoffset) { @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { super.drawScreen(mouseX, mouseY, partialTicks); - - // Draw fullscreen button on top of everything when on script editor tab - if (this.activeTab > 0) { - fullscreenButton.draw(mouseX, mouseY); - } } // ==================== MOUSE HANDLING ==================== - @Override @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { // Check if click is within autocomplete menu bounds and consume it if so GuiScriptTextArea activeArea = getActiveScriptArea(); - if (activeArea != null && activeArea.isClickInAutocompleteMenu(mouseX, mouseY)) { + boolean isOverAutocomplete = activeArea != null + && activeArea.isPointOnAutocompleteMenu(mouseX, mouseY); + if (isOverAutocomplete) { activeArea.mouseClicked(mouseX, mouseY, mouseButton); return; } // Check fullscreen button first when on script editor tab // BUT only if autocomplete is not visible (don't let clicks pass through autocomplete menu) - if (this.activeTab > 0 && (activeArea == null || !activeArea.isAutocompleteVisible()) + if (this.activeTab > 0 && !isOverAutocomplete && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { return; } 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 5f6aa0653..23b5369e4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -47,6 +47,8 @@ * - CursorNavigation: cursor movement logic */ public class GuiScriptTextArea extends GuiNpcTextField { + + private GuiScriptInterface parent; // ==================== DIMENSIONS & POSITION ==================== public int x; @@ -129,6 +131,9 @@ private int getPaddedLineCount() { public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { super(id, guiScreen, x, y, width, height, null); init(x, y, width, height, text); + + if (guiScreen instanceof GuiScriptInterface) + this.parent = (GuiScriptInterface) guiScreen; } public void init(int x, int y, int width, int height, String text) { @@ -961,6 +966,9 @@ public void drawTextBox(int xMouse, int yMouse) { drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); } + if (parent != null) + parent.fullscreenButton.draw(xMouse, yMouse); + // Draw search/replace bar (overlays viewport) searchBar.draw(xMouse, yMouse); @@ -2496,7 +2504,7 @@ private void clampSelectionBounds() { * Check if a click position is within the bounds of the autocomplete menu. * Returns false if autocomplete is not visible. */ - public boolean isClickInAutocompleteMenu(int mouseX, int mouseY) { + public boolean isPointOnAutocompleteMenu(int mouseX, int mouseY) { if (autocompleteManager == null || !autocompleteManager.isVisible()) { return false; } From 02ce0912c7c886304b6506981129a34af5812071 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 02:22:23 +0200 Subject: [PATCH 175/337] Got backspace + autocomplete (CTRL SPACE) working, now functions when cursor mid sentence/word --- .../gui/util/script/autocomplete/AutocompleteManager.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index bc725806f..630bf8c7a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -608,12 +608,20 @@ private String findReceiverExpression(String text, int dotPos) { private int findDotBeforeWhitespace(String text, int fromPos) { int pos = fromPos; // Skip backwards over whitespace + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { pos--; } + // Check if we found a dot if (pos >= 0 && text.charAt(pos) == '.') { return pos; + } else { + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) + pos--; + + if (pos >= 0 && text.charAt(pos) == '.') + return pos; } return -1; } From ff7fbd411d32e596d9183ea7fff08d9ac72c47b4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 02:39:27 +0200 Subject: [PATCH 176/337] Smart tab completion: replace till next separator --- .../client/gui/util/GuiScriptTextArea.java | 10 ++++++ .../autocomplete/AutocompleteManager.java | 35 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) 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 23b5369e4..de4ef92b6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -489,6 +489,16 @@ public void insertText(String text, int startPosition) { scrollToCursor(); } + @Override + public void replaceTextRange(String text, int startPosition, int endPosition) { + // Replace text from startPosition to endPosition + String before = GuiScriptTextArea.this.text.substring(0, startPosition); + String after = GuiScriptTextArea.this.text.substring(endPosition); + setText(before + text + after); + selection.reset(startPosition + text.length()); + scrollToCursor(); + } + @Override public int getCursorPosition() { return selection.getCursorPosition(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 630bf8c7a..d2c4aa016 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -67,6 +67,14 @@ public interface InsertCallback { */ void insertText(String text, int startPosition); + /** + * Replace text in a specific range. + * @param text Text to insert + * @param startPosition Position to start replacing from + * @param endPosition Position to end replacing at + */ + void replaceTextRange(String text, int startPosition, int endPosition); + /** * Get current cursor position. */ @@ -478,7 +486,32 @@ private void handleItemSelected(AutocompleteItem item) { } String insertText = item.getInsertText(); - insertCallback.insertText(insertText, prefixStartPosition); + + // Smart tab completion: replace till next separator + // Find the end position (current word till next separator) + String text = insertCallback.getText(); + int cursorPos = insertCallback.getCursorPosition(); + int endPos = cursorPos; + + // Extend to consume rest of current word + while (endPos < text.length() && Character.isJavaIdentifierPart(text.charAt(endPos))) { + endPos++; + } + + // Also consume following parentheses and their content if present + if (endPos < text.length() && text.charAt(endPos) == '(') { + int parenDepth = 1; + endPos++; // Skip opening paren + while (endPos < text.length() && parenDepth > 0) { + char c = text.charAt(endPos); + if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + endPos++; + } + } + + // Replace from prefixStart to endPos using the new method + insertCallback.replaceTextRange(insertText, prefixStartPosition, endPos); active = false; } From 5332774c3d12fdf60d0ea9596862c217c40e44c9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 02:39:39 +0200 Subject: [PATCH 177/337] Made ENUM constants highest priority in autocomplete dropdown menu --- .../client/gui/util/script/autocomplete/AutocompleteItem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index b5ddc7ae5..58e38aa30 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -16,12 +16,12 @@ public class AutocompleteItem implements Comparable { * The kind of completion item. */ public enum Kind { + ENUM_CONSTANT(-1), // Enum constant value (highest priority for enums) METHOD(0), // Method or function FIELD(1), // Field or property VARIABLE(2), // Local variable or parameter CLASS(3), // Class or interface type ENUM(4), // Enum type - ENUM_CONSTANT(5), // Enum constant value KEYWORD(6), // Language keyword SNIPPET(7); // Code snippet From acae8d84d0682371b8d5124c127750007bd0d76d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 03:44:07 +0200 Subject: [PATCH 178/337] Removed duplicate enums --- .../autocomplete/JavaAutocompleteProvider.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index d3b64d877..23f6e6aea 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -113,18 +113,6 @@ private void addMemberSuggestions(Context context, List items) } } - // Add enum constants if it's an enum - if (clazz.isEnum()) { - for (Object constant : clazz.getEnumConstants()) { - String constantName = constant.toString(); - items.add(new AutocompleteItem.Builder() - .name(constantName) - .insertText(constantName) - .kind(AutocompleteItem.Kind.ENUM_CONSTANT) - .typeLabel(receiverType.getSimpleName()) - .build()); - } - } } /** From 04a0fbc84db1ccc078381fc3b2fc0ebd0607bd89 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 03:48:10 +0200 Subject: [PATCH 179/337] CRAZY !!! ClassIndex for class auto-import suggestions by matching prefix i.e. Typing "Ent" suggests "net.Minecraft.entity.Entity" and all other classes that start with Ent --- .../java/noppes/npcs/client/ClientProxy.java | 2 + .../interpreter/type/CLASSINDEX_USAGE.txt | 20 ++ .../script/interpreter/type/ClassIndex.java | 278 ++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/CLASSINDEX_USAGE.txt create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index da455f5a0..c5269c089 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -124,6 +124,7 @@ import noppes.npcs.client.gui.script.GuiScriptGlobal; import noppes.npcs.client.gui.script.GuiScriptItem; import noppes.npcs.client.gui.util.script.PackageFinder; +import noppes.npcs.client.gui.util.script.interpreter.type.ClassIndex; import noppes.npcs.client.model.ModelNPCGolem; import noppes.npcs.client.model.ModelNpcCrystal; import noppes.npcs.client.model.ModelNpcDragon; @@ -739,6 +740,7 @@ public boolean isGUIOpen() { public void buildPackageIndex() { try { PackageFinder.init(Thread.currentThread().getContextClassLoader()); + ClassIndex.init(); } catch (IOException e) { } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/CLASSINDEX_USAGE.txt b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/CLASSINDEX_USAGE.txt new file mode 100644 index 000000000..2adbda50f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/CLASSINDEX_USAGE.txt @@ -0,0 +1,20 @@ +/** + * Example Usage of ClassIndex: + * + * 1. Add a single class: + * ClassIndex.getInstance().addClass(EntityPlayer.class); + * + * 2. Add an entire package recursively: + * ClassIndex.getInstance().addPackage("net.minecraft.entity.player"); + * This will scan and add ALL classes in that package and subpackages. + * + * 3. Find classes by prefix: + * List matches = ClassIndex.getInstance().findByPrefix("Entity", 50); + * // Returns fully-qualified names like "net.minecraft.entity.Entity", etc. + * + * Benefits: + * - No more manual string pairs! Just pass Class objects. + * - Simple name and package are extracted automatically via reflection. + * - Can scan entire package trees recursively with one call. + * - Cleaner, more maintainable, and easier to extend. + */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java new file mode 100644 index 000000000..8750f16ab --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java @@ -0,0 +1,278 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import java.io.File; +import java.net.URL; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * Index of Java classes for autocomplete suggestions. + * Uses reflection to automatically extract class names and packages. + * Supports multiple classes with the same simple name (e.g., java.util.Date and java.sql.Date). + */ +public class ClassIndex { + private static ClassIndex instance; + + // Map simple names to multiple classes (handles name collisions) + private final Map>> simpleNameToClasses = new LinkedHashMap<>(); + private final Set> registeredClasses = new HashSet<>(); + + private ClassIndex() { + // Initialize with common classes + initializeCommonClasses(); + } + + public static ClassIndex getInstance() { + if (instance == null) { + instance = new ClassIndex(); + } + return instance; + } + + public static void init(){ + getInstance(); + } + /** + * Register a single class by its Class object. + * The simple name and full name are extracted via reflection. + * Supports multiple classes with the same simple name. + */ + public void addClass(Class clazz) { + if (clazz == null) return; + + // Check if already registered using the Class object itself + if (registeredClasses.contains(clazz)) { + return; // Already registered + } + + registeredClasses.add(clazz); + String simpleName = clazz.getSimpleName(); + + // Only add if simple name is not empty (avoid inner classes with $ in name) + if (!simpleName.isEmpty() && !simpleName.contains("$")) { + // Add to list of classes with this simple name + simpleNameToClasses.computeIfAbsent(simpleName, k -> new ArrayList<>()).add(clazz); + } + } + + /** + * Register all classes in a package and all subpackages recursively. + * Uses ClassLoader to scan the classpath. + */ + public void addPackage(String packageName) { + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + String path = packageName.replace('.', '/'); + Enumeration resources = classLoader.getResources(path); + + Set classNames = new HashSet<>(); + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + + if (resource.getProtocol().equals("file")) { + // Scan directory + File directory = new File(resource.getFile()); + scanDirectory(directory, packageName, classNames); + } else if (resource.getProtocol().equals("jar")) { + // Scan JAR file + String jarPath = resource.getPath(); + if (jarPath.startsWith("file:")) { + jarPath = jarPath.substring(5); + } + int separatorIndex = jarPath.indexOf("!"); + if (separatorIndex != -1) { + jarPath = jarPath.substring(0, separatorIndex); + } + scanJar(jarPath, packageName, classNames); + } + } + + // Load all discovered classes + for (String className : classNames) { + try { + Class clazz = Class.forName(className, false, classLoader); + addClass(clazz); + } catch (ClassNotFoundException | NoClassDefFoundError | ExceptionInInitializerError e) { + // Skip classes that can't be loaded + } + } + } catch (Exception e) { + // Silently fail - package might not exist + } + } + + private void scanDirectory(File directory, String packageName, Set classNames) { + if (!directory.exists() || !directory.isDirectory()) { + return; + } + + File[] files = directory.listFiles(); + if (files == null) { + return; + } + + for (File file : files) { + String fileName = file.getName(); + + if (file.isDirectory()) { + // Recursively scan subdirectory + scanDirectory(file, packageName + "." + fileName, classNames); + } else if (fileName.endsWith(".class")) { + // Add class name + String className = packageName + "." + fileName.substring(0, fileName.length() - 6); + classNames.add(className); + } + } + } + + private void scanJar(String jarPath, String packageName, Set classNames) { + try { + JarFile jarFile = new JarFile(jarPath); + Enumeration entries = jarFile.entries(); + String packagePath = packageName.replace('.', '/'); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (entryName.startsWith(packagePath) && entryName.endsWith(".class")) { + // Convert path to class name + String className = entryName + .substring(0, entryName.length() - 6) + .replace('/', '.'); + classNames.add(className); + } + } + + jarFile.close(); + } catch (Exception e) { + // Silently fail + } + } + + /** + * Find all classes whose simple name starts with the given prefix (case-insensitive). + * Returns fully-qualified class names for all matches, including multiple classes with the same simple name. + */ + public List findByPrefix(String prefix, int maxResults) { + List results = new ArrayList<>(); + String lowerPrefix = prefix.toLowerCase(); + + for (Map.Entry>> entry : simpleNameToClasses.entrySet()) { + if (entry.getKey().toLowerCase().startsWith(lowerPrefix)) { + // Add all classes with this simple name + for (Class clazz : entry.getValue()) { + results.add(clazz.getName()); + + if (results.size() >= maxResults) { + return results; + } + } + } + } + + return results; + } + + /** + * Initialize the index with commonly used Java, Minecraft, and NPC classes. + */ + private void initializeCommonClasses() { + // Java standard library + addClass(String.class); + addClass(Integer.class); + addClass(Double.class); + addClass(Float.class); + addClass(Long.class); + addClass(Boolean.class); + addClass(Character.class); + addClass(Byte.class); + addClass(Short.class); + addClass(Object.class); + addClass(Math.class); + addClass(System.class); + addClass(Thread.class); + addClass(Runnable.class); + addClass(Exception.class); + addClass(RuntimeException.class); + addClass(Error.class); + addClass(Throwable.class); + + // Java collections + addClass(List.class); + addClass(ArrayList.class); + addClass(LinkedList.class); + addClass(Set.class); + addClass(HashSet.class); + addClass(LinkedHashSet.class); + addClass(TreeSet.class); + addClass(Map.class); + addClass(HashMap.class); + addClass(LinkedHashMap.class); + addClass(TreeMap.class); + addClass(Collection.class); + addClass(Iterator.class); + addClass(Comparator.class); + addClass(Collections.class); + addClass(Arrays.class); + addClass(Queue.class); + addClass(Deque.class); + addClass(Stack.class); + addClass(Vector.class); + addClass(Hashtable.class); + + // Java I/O + addClass(java.io.File.class); + addClass(java.io.InputStream.class); + addClass(java.io.OutputStream.class); + addClass(java.io.Reader.class); + addClass(java.io.Writer.class); + addClass(java.io.BufferedReader.class); + addClass(java.io.BufferedWriter.class); + addClass(java.io.FileReader.class); + addClass(java.io.FileWriter.class); + addClass(java.io.IOException.class); + + // Java utilities + addClass(java.util.Random.class); + addClass(java.util.Date.class); + addClass(java.util.Calendar.class); + addClass(java.util.UUID.class); + addClass(java.util.regex.Pattern.class); + addClass(java.util.regex.Matcher.class); + addPackage("java.util.function"); + + // Now add common Minecraft/Forge/NPC packages + addPackage("net.minecraft.entity"); + addPackage("net.minecraft.item"); + addPackage("net.minecraft.block"); + addPackage("net.minecraft.world"); + addPackage("net.minecraft.util"); + addPackage("net.minecraft.nbt"); + addPackage("net.minecraft.potion"); + addPackage("net.minecraft.enchantment"); + addPackage("net.minecraft.inventory"); + addPackage("net.minecraft.tileentity"); + addPackage("net.minecraft.command"); + addPackage("net.minecraft.client"); + + // Forge + addPackage("net.minecraftforge.common"); + addPackage("net.minecraftforge.event"); + addPackage("net.minecraftforge.fml.common"); + + // CustomNPCs + addPackage("noppes.npcs"); + + // DBC (if available) + try { + addPackage("JinRyuu.JRMCore"); + addPackage("JinRyuu.DragonBC"); + addPackage("kamkeel.npcdbc"); + } catch (Exception e) { + // DBC might not be available + } + } +} \ No newline at end of file From ac922107715e993b3055cb084d49dc4281b909ff Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 03:59:51 +0200 Subject: [PATCH 180/337] Implemented AUTO IMPORT SUGGESTION COMPLETIONS --- .../client/gui/util/GuiScriptTextArea.java | 131 ++++++++++++++++++ .../script/autocomplete/AutocompleteItem.java | 49 +++++-- .../autocomplete/AutocompleteManager.java | 14 +- .../JavaAutocompleteProvider.java | 63 +++++++++ .../script/interpreter/type/TypeResolver.java | 23 +++ 5 files changed, 271 insertions(+), 9 deletions(-) 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 de4ef92b6..f49547c2e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -499,6 +499,12 @@ public void replaceTextRange(String text, int startPosition, int endPosition) { scrollToCursor(); } + @Override + public void addImport(String importPath) { + // Add import statement and sort all imports + addAndSortImport(importPath); + } + @Override public int getCursorPosition() { return selection.getCursorPosition(); @@ -2534,6 +2540,131 @@ public boolean isPointOnAutocompleteMenu(int mouseX, int mouseY) { mouseY >= menuY && mouseY <= menuY + menuHeight; } + // ==================== AUTO-IMPORT ==================== + + /** + * Add an import statement and sort all imports. + */ + private void addAndSortImport(String importPath) { + String currentText = this.text; + int savedCursorPos = selection.getCursorPosition(); + + // Find all existing imports + java.util.regex.Pattern importPattern = java.util.regex.Pattern.compile( + "(?m)^\\s*import\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_*][A-Za-z0-9_]*)*)\\s*;\\s*$" + ); + java.util.regex.Matcher matcher = importPattern.matcher(currentText); + + java.util.List imports = new java.util.ArrayList<>(); + int firstImportStart = -1; + int lastImportEnd = -1; + + while (matcher.find()) { + String importStatement = matcher.group(0); + String importPathFound = matcher.group(1).replaceAll("\\s+", ""); + + if (firstImportStart == -1) { + firstImportStart = matcher.start(); + } + lastImportEnd = matcher.end(); + + // Skip if this is the import we're trying to add + if (!importPathFound.equals(importPath)) { + imports.add(new ImportEntry(importPathFound, importStatement.trim())); + } + } + + // Add the new import + imports.add(new ImportEntry(importPath, "import " + importPath + ";")); + + // Sort imports + java.util.Collections.sort(imports, new java.util.Comparator() { + @Override + public int compare(ImportEntry a, ImportEntry b) { + // Sort order: java.*, javax.*, then others alphabetically + boolean aIsJava = a.path.startsWith("java."); + boolean aIsJavax = a.path.startsWith("javax."); + boolean bIsJava = b.path.startsWith("java."); + boolean bIsJavax = b.path.startsWith("javax."); + + if (aIsJava && !bIsJava) return -1; + if (!aIsJava && bIsJava) return 1; + if (aIsJavax && !bIsJavax && !bIsJava) return -1; + if (!aIsJavax && bIsJavax && !aIsJava) return 1; + + return a.path.compareTo(b.path); + } + }); + + // Build the new import block + StringBuilder importBlock = new StringBuilder(); + String prevPackage = ""; + for (ImportEntry entry : imports) { + // Add blank line between different top-level packages + String topPackage = entry.path.contains(".") ? + entry.path.substring(0, entry.path.indexOf('.')) : entry.path; + if (!prevPackage.isEmpty() && !topPackage.equals(prevPackage)) { + importBlock.append("\n"); + } + importBlock.append(entry.statement).append("\n"); + prevPackage = topPackage; + } + + // Determine where to insert/replace imports + String newText; + int cursorAdjustment = 0; + + if (firstImportStart != -1) { + // Replace existing import block + String before = currentText.substring(0, firstImportStart); + String after = currentText.substring(lastImportEnd); + newText = before + importBlock.toString() + after; + + // Adjust cursor if it's after the import block + int newImportEnd = firstImportStart + importBlock.length(); + if (savedCursorPos >= lastImportEnd) { + cursorAdjustment = newImportEnd - lastImportEnd; + } + } else { + // No existing imports - add at top after package statement (if any) + java.util.regex.Pattern packagePattern = java.util.regex.Pattern.compile( + "(?m)^\\s*package\\s+[A-Za-z_][A-Za-z0-9_.]*\\s*;\\s*$" + ); + java.util.regex.Matcher pkgMatcher = packagePattern.matcher(currentText); + + int insertPos = 0; + if (pkgMatcher.find()) { + insertPos = pkgMatcher.end(); + // Add blank line after package + newText = currentText.substring(0, insertPos) + "\n" + + importBlock.toString() + "\n" + currentText.substring(insertPos); + cursorAdjustment = importBlock.length() + 2; // +2 for the newlines + } else { + // Insert at very beginning + newText = importBlock.toString() + "\n" + currentText; + cursorAdjustment = importBlock.length() + 1; + } + } + + // Apply the changes + setText(newText); + selection.reset(savedCursorPos + cursorAdjustment); + scrollToCursor(); + } + + /** + * Helper class for tracking imports during sorting. + */ + private static class ImportEntry { + String path; + String statement; + + ImportEntry(String path, String statement) { + this.path = path; + this.statement = statement; + } + } + // ==================== INNER CLASSES ==================== public static class UndoData { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 58e38aa30..96da52b9d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -45,12 +45,17 @@ public int getPriority() { private final Object sourceData; // Original source (MethodInfo, FieldInfo, etc.) private final boolean deprecated; // Whether this item is deprecated + // Import tracking + private final boolean requiresImport; // Whether selecting this item requires adding an import + private final String importPath; // Full path for import (e.g., "net.minecraft.client.Minecraft") + // Match scoring private int matchScore = 0; // How well this matches the query private int[] matchIndices; // Indices of matched characters for highlighting private AutocompleteItem(String name, String insertText, Kind kind, String typeLabel, - String signature, String documentation, Object sourceData, boolean deprecated) { + String signature, String documentation, Object sourceData, boolean deprecated, + boolean requiresImport, String importPath) { this.name = name; this.insertText = insertText; this.kind = kind; @@ -59,6 +64,8 @@ private AutocompleteItem(String name, String insertText, Kind kind, String typeL this.documentation = documentation; this.sourceData = sourceData; this.deprecated = deprecated; + this.requiresImport = requiresImport; + this.importPath = importPath; } // ==================== FACTORY METHODS ==================== @@ -105,7 +112,9 @@ public static AutocompleteItem fromMethod(MethodInfo method) { signature, method.getDocumentation(), method, - false // TODO: Check for @Deprecated annotation + false, // TODO: Check for @Deprecated annotation + false, // Methods don't require imports + null ); } @@ -139,7 +148,9 @@ public static AutocompleteItem fromField(FieldInfo field) { typeLabel + " " + field.getName(), null, field, - false + false, + false, // Fields don't require imports + null ); } @@ -167,7 +178,9 @@ public static AutocompleteItem fromType(TypeInfo type) { type.getFullName(), null, type, - false + false, + false, // Will be overridden for unimported types + null ); } @@ -192,7 +205,9 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { method.getSignature(), method.getDocumentation(), method, - false + false, + false, + null ); } @@ -208,7 +223,9 @@ public static AutocompleteItem fromJSField(JSFieldInfo field) { field.toString(), field.getDocumentation(), field, - false + false, + false, + null ); } @@ -224,7 +241,9 @@ public static AutocompleteItem keyword(String keyword) { null, null, null, - false + false, + false, + null ); } @@ -339,6 +358,8 @@ public int calculateMatchScore(String query) { public String getDocumentation() { return documentation; } public Object getSourceData() { return sourceData; } public boolean isDeprecated() { return deprecated; } + public boolean requiresImport() { return requiresImport; } + public String getImportPath() { return importPath; } public int getMatchScore() { return matchScore; } public int[] getMatchIndices() { return matchIndices; } @@ -409,6 +430,8 @@ public static class Builder { private String documentation; private Object sourceData; private boolean deprecated = false; + private boolean requiresImport = false; + private String importPath = null; public Builder name(String name) { this.name = name; @@ -450,12 +473,22 @@ public Builder deprecated(boolean deprecated) { return this; } + public Builder requiresImport(boolean requiresImport) { + this.requiresImport = requiresImport; + return this; + } + + public Builder importPath(String importPath) { + this.importPath = importPath; + return this; + } + public AutocompleteItem build() { if (insertText == null) { insertText = name; } return new AutocompleteItem(name, insertText, kind, typeLabel, - signature, documentation, sourceData, deprecated); + signature, documentation, sourceData, deprecated, requiresImport, importPath); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index d2c4aa016..ace3cab81 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -75,6 +75,12 @@ public interface InsertCallback { */ void replaceTextRange(String text, int startPosition, int endPosition); + /** + * Add an import statement and sort all imports. + * @param importPath Full path of the class to import (e.g., "net.minecraft.client.Minecraft") + */ + void addImport(String importPath); + /** * Get current cursor position. */ @@ -510,9 +516,15 @@ private void handleItemSelected(AutocompleteItem item) { } } - // Replace from prefixStart to endPos using the new method + // IMPORTANT: Do the text replacement FIRST, then add import + // This prevents position corruption since import adds text at the TOP insertCallback.replaceTextRange(insertText, prefixStartPosition, endPos); + // If this item requires an import, add it AFTER the text replacement + if (item.requiresImport() && item.getImportPath() != null) { + insertCallback.addImport(item.getImportPath()); + } + active = false; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 23f6e6aea..4126d8526 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -224,10 +224,73 @@ private void addScopeSuggestions(Context context, List items) items.add(AutocompleteItem.fromType(scriptType)); } + // Add unimported classes that match the prefix (for auto-import) + if (context.prefix != null && context.prefix.length() >= 2 && Character.isUpperCase(context.prefix.charAt(0))) { + addUnimportedClassSuggestions(context.prefix, items); + } + // Add keywords addKeywords(items); } + /** + * Add suggestions for unimported classes that match the prefix. + * These will trigger auto-import when selected. + */ + private void addUnimportedClassSuggestions(String prefix, List items) { + // Get the type resolver + TypeResolver resolver = TypeResolver.getInstance(); + + // Find classes matching this prefix (not just exact matches) + List matchingClasses = resolver.findClassesByPrefix(prefix, 15); + + // Track what's already imported to avoid duplicates + Set importedFullNames = new HashSet<>(); + for (TypeInfo imported : document.getImportedTypes()) { + importedFullNames.add(imported.getFullName()); + } + + // Also track simple names already in the list to avoid showing both + // imported and unimported versions of the same class + Set existingSimpleNames = new HashSet<>(); + for (AutocompleteItem item : items) { + if (item.getKind() == AutocompleteItem.Kind.CLASS || + item.getKind() == AutocompleteItem.Kind.ENUM) { + existingSimpleNames.add(item.getName()); + } + } + + for (String fullName : matchingClasses) { + // Skip if already imported + if (importedFullNames.contains(fullName)) { + continue; + } + + TypeInfo type = resolver.resolveFullName(fullName); + if (type != null && type.isResolved()) { + // Skip if a class with this simple name is already in the list + if (existingSimpleNames.contains(type.getSimpleName())) { + continue; + } + + // Create an item that requires import + AutocompleteItem item = new AutocompleteItem.Builder() + .name(type.getSimpleName()) + .insertText(type.getSimpleName()) + .kind(type.getKind() == TypeInfo.Kind.ENUM ? + AutocompleteItem.Kind.ENUM : AutocompleteItem.Kind.CLASS) + .typeLabel(type.getPackageName()) + .signature(type.getFullName()) + .sourceData(type) + .requiresImport(true) + .importPath(fullName) + .build(); + items.add(item); + existingSimpleNames.add(type.getSimpleName()); + } + } + } + /** * Add Java keywords. */ 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 b686594a1..1be0617d7 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 @@ -391,6 +391,29 @@ public static boolean isModifier(String word) { word.equals("native") || word.equals("strictfp"); } + // ==================== CLASS SEARCH ==================== + + /** + * Search for classes matching the given prefix. + * Uses ClassIndex for O(n) lookup where n is the number of indexed classes. + * + * @param prefix The prefix to match (case-sensitive for class names) + * @param maxResults Maximum number of results to return + * @return List of fully-qualified class names that match + */ + public List findClassesByPrefix(String prefix, int maxResults) { + return ClassIndex.getInstance().findByPrefix(prefix, maxResults); + } + + /** + * @deprecated Use findClassesByPrefix instead for prefix matching + */ + @Deprecated + public List findClassesBySimpleName(String simpleName, int maxResults) { + // Redirect to prefix matching for backwards compatibility + return findClassesByPrefix(simpleName, maxResults); + } + /** * Represents a type occurrence within generic content. */ From a4960614550207494399adb29d44809a5a7f099b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 16:00:48 +0200 Subject: [PATCH 181/337] Added auto complete strict matching for non-member access first word. i.e "Event" can only show words that start with Event, not contains it --- .../script/autocomplete/AutocompleteItem.java | 51 +++++++++++++++---- .../JavaAutocompleteProvider.java | 15 +++--- .../script/interpreter/type/ClassIndex.java | 2 +- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 96da52b9d..768abf2a3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -36,7 +36,8 @@ public int getPriority() { } } - private final String name; // Display name (e.g., "getPlayer") + private final String name; // Display name (e.g., "getPlayer" or "getPlayer(UUID id)" for methods) + private final String searchName; // Name to search against (just the identifier, e.g., "getPlayer") private final String insertText; // Text to insert (e.g., "getPlayer()") private final Kind kind; // Type of completion private final String typeLabel; // Type label (e.g., "IPlayer", "void") @@ -53,10 +54,11 @@ public int getPriority() { private int matchScore = 0; // How well this matches the query private int[] matchIndices; // Indices of matched characters for highlighting - private AutocompleteItem(String name, String insertText, Kind kind, String typeLabel, + private AutocompleteItem(String name, String searchName, String insertText, Kind kind, String typeLabel, String signature, String documentation, Object sourceData, boolean deprecated, boolean requiresImport, String importPath) { this.name = name; + this.searchName = searchName != null ? searchName : name; // Default to name if not provided this.insertText = insertText; this.kind = kind; this.typeLabel = typeLabel; @@ -106,6 +108,7 @@ public static AutocompleteItem fromMethod(MethodInfo method) { return new AutocompleteItem( displayName.toString(), + name, // Search against just the method name, not the params insertText.toString(), Kind.METHOD, returnType, @@ -142,6 +145,7 @@ public static AutocompleteItem fromField(FieldInfo field) { return new AutocompleteItem( field.getName(), + field.getName(), // searchName same as display name for fields field.getName(), kind, typeLabel, @@ -172,6 +176,7 @@ public static AutocompleteItem fromType(TypeInfo type) { return new AutocompleteItem( type.getSimpleName(), + type.getSimpleName(), // searchName same as display name for types type.getSimpleName(), kind, type.getPackageName(), @@ -199,6 +204,7 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { return new AutocompleteItem( displayName, + name, // Search against just the method name insertText.toString(), Kind.METHOD, method.getReturnType(), @@ -217,6 +223,7 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { public static AutocompleteItem fromJSField(JSFieldInfo field) { return new AutocompleteItem( field.getName(), + field.getName(), // searchName same as display name field.getName(), Kind.FIELD, field.getType(), @@ -235,6 +242,7 @@ public static AutocompleteItem fromJSField(JSFieldInfo field) { public static AutocompleteItem keyword(String keyword) { return new AutocompleteItem( keyword, + keyword, // searchName same as display name keyword, Kind.KEYWORD, "keyword", @@ -272,15 +280,18 @@ private static String buildMethodSignature(MethodInfo method) { /** * Calculate fuzzy match score against a query string. * Returns -1 if no match, or a positive score (higher = better match). + * + * @param query The search query + * @param requirePrefix If true, only match items that start with the query (no fuzzy/contains matching) */ - public int calculateMatchScore(String query) { + public int calculateMatchScore(String query, boolean requirePrefix) { if (query == null || query.isEmpty()) { matchScore = 100; // Everything matches empty query matchIndices = new int[0]; return matchScore; } - String lowerName = name.toLowerCase(); + String lowerName = searchName.toLowerCase(); String lowerQuery = query.toLowerCase(); // Exact prefix match is best @@ -293,6 +304,13 @@ public int calculateMatchScore(String query) { return matchScore; } + // If requirePrefix is true, stop here - no fuzzy/substring matching + if (requirePrefix) { + matchScore = -1; + matchIndices = new int[0]; + return matchScore; + } + // Exact substring match int subIndex = lowerName.indexOf(lowerQuery); if (subIndex >= 0) { @@ -312,8 +330,8 @@ public int calculateMatchScore(String query) { int consecutiveBonus = 0; int lastMatchIdx = -2; - while (queryIdx < query.length() && nameIdx < name.length()) { - if (Character.toLowerCase(name.charAt(nameIdx)) == lowerQuery.charAt(queryIdx)) { + while (queryIdx < query.length() && nameIdx < searchName.length()) { + if (Character.toLowerCase(searchName.charAt(nameIdx)) == lowerQuery.charAt(queryIdx)) { indices[queryIdx] = nameIdx; // Bonus for consecutive matches @@ -322,9 +340,9 @@ public int calculateMatchScore(String query) { } // Bonus for matching at word boundaries (camelCase) - if (nameIdx == 0 || !Character.isLetterOrDigit(name.charAt(nameIdx - 1)) || - (Character.isUpperCase(name.charAt(nameIdx)) && nameIdx > 0 && - Character.isLowerCase(name.charAt(nameIdx - 1)))) { + if (nameIdx == 0 || !Character.isLetterOrDigit(searchName.charAt(nameIdx - 1)) || + (Character.isUpperCase(searchName.charAt(nameIdx)) && nameIdx > 0 && + Character.isLowerCase(searchName.charAt(nameIdx - 1)))) { consecutiveBonus += 20; } @@ -348,6 +366,13 @@ public int calculateMatchScore(String query) { return matchScore; } + /** + * Backward compatibility overload - defaults to fuzzy matching (no prefix requirement). + */ + public int calculateMatchScore(String query) { + return calculateMatchScore(query, false); + } + // ==================== GETTERS ==================== public String getName() { return name; } @@ -423,6 +448,7 @@ public String toString() { */ public static class Builder { private String name; + private String searchName; private String insertText; private Kind kind = Kind.FIELD; private String typeLabel = ""; @@ -438,6 +464,11 @@ public Builder name(String name) { return this; } + public Builder searchName(String searchName) { + this.searchName = searchName; + return this; + } + public Builder insertText(String insertText) { this.insertText = insertText; return this; @@ -487,7 +518,7 @@ public AutocompleteItem build() { if (insertText == null) { insertText = name; } - return new AutocompleteItem(name, insertText, kind, typeLabel, + return new AutocompleteItem(name, searchName, insertText, kind, typeLabel, signature, documentation, sourceData, deprecated, requiresImport, importPath); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 4126d8526..d1fc2c017 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -46,7 +46,7 @@ public List getSuggestions(Context context) { } // Filter and score by prefix - filterAndScore(items, context.prefix); + filterAndScore(items, context.prefix, context.isMemberAccess); // Sort by score Collections.sort(items); @@ -240,9 +240,8 @@ private void addScopeSuggestions(Context context, List items) private void addUnimportedClassSuggestions(String prefix, List items) { // Get the type resolver TypeResolver resolver = TypeResolver.getInstance(); - // Find classes matching this prefix (not just exact matches) - List matchingClasses = resolver.findClassesByPrefix(prefix, 15); + List matchingClasses = resolver.findClassesByPrefix(prefix, -1); // Track what's already imported to avoid duplicates Set importedFullNames = new HashSet<>(); @@ -335,20 +334,24 @@ private boolean isStaticAccess(String receiverExpr, int position) { /** * Filter items by prefix and calculate match scores. */ - private void filterAndScore(List items, String prefix) { + private void filterAndScore(List items, String prefix, boolean isMemberAccess) { if (prefix == null || prefix.isEmpty()) { // No filtering needed, all items get a base score for (AutocompleteItem item : items) { - item.calculateMatchScore(""); + item.calculateMatchScore("", false); } return; } + // For non-member access (first word), require strict prefix matching + // For member access (after dot), allow fuzzy/contains matching + boolean requirePrefix = !isMemberAccess; + // Filter and score Iterator iter = items.iterator(); while (iter.hasNext()) { AutocompleteItem item = iter.next(); - int score = item.calculateMatchScore(prefix); + int score = item.calculateMatchScore(prefix, requirePrefix); if (score < 0) { iter.remove(); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java index 8750f16ab..7a7829fe6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java @@ -166,7 +166,7 @@ public List findByPrefix(String prefix, int maxResults) { for (Class clazz : entry.getValue()) { results.add(clazz.getName()); - if (results.size() >= maxResults) { + if (maxResults != -1 && results.size() >= maxResults) { return results; } } From 7b7667f2c56d50b92ce730c82987441dc7354afa Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 17:35:25 +0200 Subject: [PATCH 182/337] Autocomplete suggestions UsageTracker implementation --- .../script/autocomplete/AutocompleteItem.java | 8 + .../autocomplete/AutocompleteManager.java | 38 +++ .../autocomplete/JSAutocompleteProvider.java | 45 ++- .../JavaAutocompleteProvider.java | 37 ++- .../script/autocomplete/UsageTracker.java | 270 ++++++++++++++++++ 5 files changed, 387 insertions(+), 11 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 768abf2a3..3aee42085 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -373,9 +373,17 @@ public int calculateMatchScore(String query) { return calculateMatchScore(query, false); } + /** + * Add a boost to the match score (e.g., from usage tracking). + */ + public void addScoreBoost(int boost) { + this.matchScore += boost; + } + // ==================== GETTERS ==================== public String getName() { return name; } + public String getSearchName() { return searchName; } public String getInsertText() { return insertText; } public Kind getKind() { return kind; } public String getTypeLabel() { return typeLabel; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index ace3cab81..360d0633c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -3,6 +3,7 @@ import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import org.lwjgl.input.Mouse; import java.util.ArrayList; @@ -53,6 +54,12 @@ public class AutocompleteManager { /** Whether this was an explicit trigger (Ctrl+Space) */ private boolean explicitTrigger = false; + /** The full class name of the current receiver type (for member access tracking) */ + private String currentReceiverFullName = null; + + /** Whether current context is member access */ + private boolean currentIsMemberAccess = false; + /** Callback for text insertion */ private InsertCallback insertCallback; @@ -360,6 +367,18 @@ private void showSuggestions(String text, int cursorPosition, String prefix, int prefixStart, boolean isMemberAccess, String receiverExpr) { if (insertCallback == null || document == null) return; + // Track context for usage recording when item is selected + currentIsMemberAccess = isMemberAccess; + currentReceiverFullName = null; + + // Resolve receiver type if member access + if (isMemberAccess && receiverExpr != null) { + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, prefixStart); + if (receiverType != null && receiverType.isResolved()) { + currentReceiverFullName = receiverType.getFullName(); + } + } + // Build context int lineNumber = getLineNumber(text, cursorPosition); String currentLine = getCurrentLine(text, cursorPosition); @@ -491,6 +510,9 @@ private void handleItemSelected(AutocompleteItem item) { return; } + // Record usage for learning + recordUsage(item); + String insertText = item.getInsertText(); // Smart tab completion: replace till next separator @@ -528,6 +550,22 @@ private void handleItemSelected(AutocompleteItem item) { active = false; } + /** + * Record that the user selected an autocomplete item for usage tracking. + */ + private void recordUsage(AutocompleteItem item) { + if (document == null) return; + + UsageTracker tracker = document.isJavaScript() ? + UsageTracker.getJSInstance() : UsageTracker.getJavaInstance(); + + // For member access, use the resolved receiver type + // For standalone items (types, keywords, variables), owner is null + String owner = currentIsMemberAccess ? currentReceiverFullName : null; + + tracker.recordUsage(item, owner); + } + // ==================== DISMISS ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index fc536102c..3fb105d7a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -44,6 +44,15 @@ public boolean canProvide(Context context) { public List getSuggestions(Context context) { List items = new ArrayList<>(); + // Resolve owner type for usage tracking + String ownerFullName = null; + if (context.isMemberAccess && context.receiverExpression != null) { + String receiverType = resolveReceiverType(context.receiverExpression); + if (receiverType != null) { + ownerFullName = receiverType; + } + } + if (context.isMemberAccess) { // Member access: resolve type of receiver and get its members addMemberSuggestions(context, items); @@ -52,8 +61,8 @@ public List getSuggestions(Context context) { addScopeSuggestions(context, items); } - // Filter and score by prefix - filterAndScore(items, context.prefix); + // Filter and score by prefix, then apply usage boosts + filterAndScore(items, context.prefix, context.isMemberAccess, ownerFullName); // Sort by score Collections.sort(items); @@ -186,6 +195,13 @@ private void addScopeSuggestions(Context context, List items) * Infer the type of an expression. */ private String inferExpressionType(String expr) { + return resolveReceiverType(expr); + } + + /** + * Resolve the type of a receiver expression for member access. + */ + private String resolveReceiverType(String expr) { if (expr == null || expr.isEmpty()) { return null; } @@ -266,23 +282,42 @@ private void addJSKeywords(List items) { } /** - * Filter items by prefix and calculate match scores. + * Filter items by prefix, calculate match scores, and apply usage boosts. */ - private void filterAndScore(List items, String prefix) { + private void filterAndScore(List items, String prefix, + boolean isMemberAccess, String ownerFullName) { + UsageTracker tracker = UsageTracker.getJSInstance(); + if (prefix == null || prefix.isEmpty()) { for (AutocompleteItem item : items) { item.calculateMatchScore(""); + applyUsageBoost(item, tracker, ownerFullName); } return; } + // For non-member access (first word), require strict prefix matching + // For member access (after dot), allow fuzzy/contains matching + boolean requirePrefix = !isMemberAccess; + Iterator iter = items.iterator(); while (iter.hasNext()) { AutocompleteItem item = iter.next(); - int score = item.calculateMatchScore(prefix); + int score = item.calculateMatchScore(prefix, requirePrefix); if (score < 0) { iter.remove(); + } else { + applyUsageBoost(item, tracker, ownerFullName); } } } + + /** + * Apply usage-based score boost to an item. + */ + private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) { + int usageCount = tracker.getUsageCount(item, ownerFullName); + int boost = UsageTracker.calculateUsageBoost(usageCount); + item.addScoreBoost(boost); + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index d1fc2c017..56456d096 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -37,6 +37,16 @@ public List getSuggestions(Context context) { return items; } + // Resolve owner type for usage tracking + String ownerFullName = null; + if (context.isMemberAccess && context.receiverExpression != null) { + TypeInfo receiverType = document.resolveExpressionType( + context.receiverExpression, context.prefixStart); + if (receiverType != null && receiverType.isResolved()) { + ownerFullName = receiverType.getFullName(); + } + } + if (context.isMemberAccess) { // Member access: resolve type of receiver and get its members addMemberSuggestions(context, items); @@ -45,8 +55,8 @@ public List getSuggestions(Context context) { addScopeSuggestions(context, items); } - // Filter and score by prefix - filterAndScore(items, context.prefix, context.isMemberAccess); + // Filter and score by prefix, then apply usage boosts + filterAndScore(items, context.prefix, context.isMemberAccess, ownerFullName); // Sort by score Collections.sort(items); @@ -332,13 +342,17 @@ private boolean isStaticAccess(String receiverExpr, int position) { } /** - * Filter items by prefix and calculate match scores. + * Filter items by prefix, calculate match scores, and apply usage boosts. */ - private void filterAndScore(List items, String prefix, boolean isMemberAccess) { + private void filterAndScore(List items, String prefix, + boolean isMemberAccess, String ownerFullName) { + UsageTracker tracker = UsageTracker.getJavaInstance(); + if (prefix == null || prefix.isEmpty()) { - // No filtering needed, all items get a base score + // No filtering needed, all items get a base score + usage boost for (AutocompleteItem item : items) { item.calculateMatchScore("", false); + applyUsageBoost(item, tracker, ownerFullName); } return; } @@ -347,17 +361,28 @@ private void filterAndScore(List items, String prefix, boolean // For member access (after dot), allow fuzzy/contains matching boolean requirePrefix = !isMemberAccess; - // Filter and score + // Filter, score, and apply usage boosts Iterator iter = items.iterator(); while (iter.hasNext()) { AutocompleteItem item = iter.next(); int score = item.calculateMatchScore(prefix, requirePrefix); if (score < 0) { iter.remove(); + } else { + applyUsageBoost(item, tracker, ownerFullName); } } } + /** + * Apply usage-based score boost to an item. + */ + private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) { + int usageCount = tracker.getUsageCount(item, ownerFullName); + int boost = UsageTracker.calculateUsageBoost(usageCount); + item.addScoreBoost(boost); + } + /** * Builder class for AutocompleteItem to handle cases without source data. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java new file mode 100644 index 000000000..6ee55bd31 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java @@ -0,0 +1,270 @@ +package noppes.npcs.client.gui.util.script.autocomplete; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import noppes.npcs.CustomNpcs; + +import java.io.*; +import java.lang.reflect.Type; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Tracks user autocomplete selections to improve suggestion ranking. + * Usage counts are persisted to JSON and used to boost frequently selected items. + * + * Key format: "ownerFullName|memberName|KIND" + * - ownerFullName: Full class name for members, empty for standalone items + * - memberName: The item name (method, field, type, etc.) + * - KIND: The item kind (METHOD, FIELD, CLASS, ENUM, etc.) + * + * Examples: + * - "net.minecraft.client.Minecraft|getMinecraft|METHOD" + * - "net.minecraft.entity.player.EntityPlayer|inventory|FIELD" + * - "|EntityPlayer|CLASS" (standalone type suggestion) + * - "|if|KEYWORD" + */ +public class UsageTracker { + + private static final String JAVA_FILE = "java_usages.json"; + private static final String JS_FILE = "js_usages.json"; + private static final long SAVE_INTERVAL_MS = 60_000; // Auto-save every 60 seconds + private static final int USAGE_SCORE_MULTIPLIER = 50; // Score boost per usage + private static final int MAX_USAGE_BOOST = 5000; // Cap the boost to prevent runaway scores + + private static UsageTracker javaInstance; + private static UsageTracker jsInstance; + private static boolean initialized = false; + + private final Map usageCounts = new ConcurrentHashMap<>(); + private final File file; + private final AtomicBoolean dirty = new AtomicBoolean(false); + private final AtomicBoolean loaded = new AtomicBoolean(false); + + private static ScheduledExecutorService scheduler; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + // ==================== SINGLETON ACCESS ==================== + + public static synchronized UsageTracker getJavaInstance() { + ensureInitialized(); + if (javaInstance == null) { + javaInstance = new UsageTracker(JAVA_FILE); + javaInstance.load(); + } + return javaInstance; + } + + public static synchronized UsageTracker getJSInstance() { + ensureInitialized(); + if (jsInstance == null) { + jsInstance = new UsageTracker(JS_FILE); + jsInstance.load(); + } + return jsInstance; + } + + /** + * Ensure the tracker system is initialized with auto-save and shutdown hook. + */ + private static void ensureInitialized() { + if (!initialized) { + initialized = true; + initialize(); + + // Add shutdown hook to save on JVM exit + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + shutdown(); + }, "UsageTracker-Shutdown")); + } + } + + /** + * Initialize the auto-save scheduler. Call once on client startup. + */ + public static void initialize() { + if (scheduler == null || scheduler.isShutdown()) { + scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "UsageTracker-AutoSave"); + t.setDaemon(true); + return t; + }); + + scheduler.scheduleAtFixedRate(() -> { + saveAllIfDirty(); + }, SAVE_INTERVAL_MS, SAVE_INTERVAL_MS, TimeUnit.MILLISECONDS); + } + } + + /** + * Shutdown and save all data. Call on client shutdown. + */ + public static void shutdown() { + saveAllIfDirty(); + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdown(); + } + } + + private static void saveAllIfDirty() { + if (javaInstance != null) { + javaInstance.saveIfDirty(); + } + if (jsInstance != null) { + jsInstance.saveIfDirty(); + } + } + + // ==================== INSTANCE METHODS ==================== + + private UsageTracker(String filename) { + File dir = getDir(); + this.file = new File(dir, filename); + } + + private File getDir() { + File dir = new File(CustomNpcs.Dir, "tracked_usages"); + if (!dir.exists()) { + dir.mkdir(); + } + return dir; + } + + /** + * Record that the user selected an autocomplete item. + * + * @param owner The owner context (full class name for members, null/empty for standalone) + * @param name The item name + * @param kind The item kind + */ + public void recordUsage(String owner, String name, AutocompleteItem.Kind kind) { + String key = buildKey(owner, name, kind); + usageCounts.merge(key, 1, Integer::sum); + dirty.set(true); + } + + /** + * Record usage directly from an AutocompleteItem. + * + * @param item The selected autocomplete item + * @param ownerFullName The full class name of the owner (for member access), or null + */ + public void recordUsage(AutocompleteItem item, String ownerFullName) { + String name = item.getSearchName() != null ? item.getSearchName() : item.getName(); + recordUsage(ownerFullName, name, item.getKind()); + } + + /** + * Get the usage count for an item. + * + * @param owner The owner context + * @param name The item name + * @param kind The item kind + * @return The number of times this item was selected + */ + public int getUsageCount(String owner, String name, AutocompleteItem.Kind kind) { + String key = buildKey(owner, name, kind); + return usageCounts.getOrDefault(key, 0); + } + + /** + * Get usage count directly from an AutocompleteItem. + */ + public int getUsageCount(AutocompleteItem item, String ownerFullName) { + String name = item.getSearchName() != null ? item.getSearchName() : item.getName(); + return getUsageCount(ownerFullName, name, item.getKind()); + } + + /** + * Calculate the score boost based on usage count. + * Uses a logarithmic scale to prevent very frequent items from dominating completely. + * + * @param usageCount The number of times the item was selected + * @return The score boost to add + */ + public static int calculateUsageBoost(int usageCount) { + if (usageCount <= 0) return 0; + + // Logarithmic scaling: log2(count + 1) * multiplier + // This gives diminishing returns for very high counts + int boost = (int) (Math.log(usageCount + 1) / Math.log(2) * USAGE_SCORE_MULTIPLIER); + return Math.min(boost, MAX_USAGE_BOOST); + } + + /** + * Build a unique key for an item. + */ + public static String buildKey(String owner, String name, AutocompleteItem.Kind kind) { + String ownerPart = owner != null ? owner : ""; + String kindPart = kind != null ? kind.name() : "UNKNOWN"; + return ownerPart + "|" + name + "|" + kindPart; + } + + // ==================== PERSISTENCE ==================== + + /** + * Load usage data from file. + */ + public void load() { + if (loaded.get()) return; + + if (!file.exists()) { + loaded.set(true); + return; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + Type type = new TypeToken>(){}.getType(); + Map data = GSON.fromJson(reader, type); + if (data != null) { + usageCounts.putAll(data); + } + loaded.set(true); + } catch (Exception e) { + System.err.println("[UsageTracker] Failed to load " + file.getName() + ": " + e.getMessage()); + loaded.set(true); // Mark as loaded even on failure to prevent retry loops + } + } + + /** + * Save usage data to file if there are pending changes. + */ + public void saveIfDirty() { + if (!dirty.compareAndSet(true, false)) { + return; // Not dirty, nothing to save + } + save(); + } + + /** + * Force save usage data to file. + */ + public void save() { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + GSON.toJson(usageCounts, writer); + } catch (Exception e) { + System.err.println("[UsageTracker] Failed to save " + file.getName() + ": " + e.getMessage()); + dirty.set(true); // Mark dirty again so we retry later + } + } + + /** + * Clear all usage data (for testing/reset purposes). + */ + public void clear() { + usageCounts.clear(); + dirty.set(true); + } + + /** + * Get the number of tracked items. + */ + public int size() { + return usageCounts.size(); + } +} From 6e602c45a935f3a12326033c40f46bdb28093280 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 17:50:27 +0200 Subject: [PATCH 183/337] Penalized auto complete static members when in a non-static context --- .../JavaAutocompleteProvider.java | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 56456d096..4d70ac397 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -37,14 +37,17 @@ public List getSuggestions(Context context) { return items; } - // Resolve owner type for usage tracking + // Resolve owner type for usage tracking and static context detection String ownerFullName = null; + boolean isStaticContext = false; if (context.isMemberAccess && context.receiverExpression != null) { TypeInfo receiverType = document.resolveExpressionType( context.receiverExpression, context.prefixStart); if (receiverType != null && receiverType.isResolved()) { ownerFullName = receiverType.getFullName(); } + // Check if accessing through a class (static context) vs instance + isStaticContext = isStaticAccess(context.receiverExpression, context.prefixStart); } if (context.isMemberAccess) { @@ -55,8 +58,8 @@ public List getSuggestions(Context context) { addScopeSuggestions(context, items); } - // Filter and score by prefix, then apply usage boosts - filterAndScore(items, context.prefix, context.isMemberAccess, ownerFullName); + // Filter and score by prefix, then apply usage boosts and static penalties + filterAndScore(items, context.prefix, context.isMemberAccess, isStaticContext, ownerFullName); // Sort by score Collections.sort(items); @@ -342,10 +345,10 @@ private boolean isStaticAccess(String receiverExpr, int position) { } /** - * Filter items by prefix, calculate match scores, and apply usage boosts. + * Filter items by prefix, calculate match scores, apply usage boosts, and penalize static members in instance contexts. */ private void filterAndScore(List items, String prefix, - boolean isMemberAccess, String ownerFullName) { + boolean isMemberAccess, boolean isStaticContext, String ownerFullName) { UsageTracker tracker = UsageTracker.getJavaInstance(); if (prefix == null || prefix.isEmpty()) { @@ -353,6 +356,7 @@ private void filterAndScore(List items, String prefix, for (AutocompleteItem item : items) { item.calculateMatchScore("", false); applyUsageBoost(item, tracker, ownerFullName); + applyStaticPenalty(item, isMemberAccess, isStaticContext); } return; } @@ -361,7 +365,7 @@ private void filterAndScore(List items, String prefix, // For member access (after dot), allow fuzzy/contains matching boolean requirePrefix = !isMemberAccess; - // Filter, score, and apply usage boosts + // Filter, score, apply usage boosts, and apply static penalties Iterator iter = items.iterator(); while (iter.hasNext()) { AutocompleteItem item = iter.next(); @@ -370,6 +374,7 @@ private void filterAndScore(List items, String prefix, iter.remove(); } else { applyUsageBoost(item, tracker, ownerFullName); + applyStaticPenalty(item, isMemberAccess, isStaticContext); } } } @@ -383,6 +388,38 @@ private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String item.addScoreBoost(boost); } + /** + * Apply penalty to static members when accessed in a non-static (instance) context. + * This matches IntelliJ's behavior where static members are deprioritized when + * accessing through an instance (e.g., Minecraft.getMinecraft().getMinecraft()). + */ + private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) { + // Only apply penalty in member access contexts (after dot) + if (!isMemberAccess) { + return; + } + + // Only penalize when we're in an instance context (not static) + if (isStaticContext) { + return; + } + + // Check if the item is a static member + Object sourceData = item.getSourceData(); + boolean isStatic = false; + + if (sourceData instanceof MethodInfo) { + isStatic = ((MethodInfo) sourceData).isStatic(); + } else if (sourceData instanceof FieldInfo) { + isStatic = ((FieldInfo) sourceData).isStatic(); + } + + // Apply penalty to static members in instance context + if (isStatic) { + item.addScoreBoost(-item.getMatchScore()); // Significant penalty to push static members down + } + } + /** * Builder class for AutocompleteItem to handle cases without source data. */ From 5bc3e5451a2911189e9a1805ac7982b41ae5a142 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 17:53:16 +0200 Subject: [PATCH 184/337] Lowered static penalty for strong prefix matches (so it pops to the top) --- .../autocomplete/JavaAutocompleteProvider.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 4d70ac397..4244d11dd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -392,6 +392,7 @@ private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String * Apply penalty to static members when accessed in a non-static (instance) context. * This matches IntelliJ's behavior where static members are deprioritized when * accessing through an instance (e.g., Minecraft.getMinecraft().getMinecraft()). + * However, if the item is a very strong match (exact prefix), don't penalize as much. */ private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) { // Only apply penalty in member access contexts (after dot) @@ -416,7 +417,16 @@ private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, b // Apply penalty to static members in instance context if (isStatic) { - item.addScoreBoost(-item.getMatchScore()); // Significant penalty to push static members down + int matchScore = item.getMatchScore(); + // Strong prefix matches (score >= 800) get a lighter penalty + // Weaker matches get pushed down more aggressively + if (matchScore >= 800) { + // Light penalty for exact prefix matches - just deprioritize slightly + item.addScoreBoost(-200); + } else { + // Heavy penalty for fuzzy/substring matches - push to bottom + item.addScoreBoost(-matchScore); + } } } From 461e45c2eb0e31f5bee336ae21a826ae20008bf4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 18:03:57 +0200 Subject: [PATCH 185/337] ClassIndex Package exclusions --- .../script/interpreter/type/ClassIndex.java | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java index 7a7829fe6..694dd40d1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassIndex.java @@ -61,12 +61,32 @@ public void addClass(Class clazz) { * Uses ClassLoader to scan the classpath. */ public void addPackage(String packageName) { + addPackage(packageName, new String[0]); + } + + /** + * Register all classes in a package and all subpackages recursively, + * excluding specified packages. + * Uses ClassLoader to scan the classpath. + * + * @param packageName The base package to scan + * @param excludedPackages Array of package names to exclude (including their subpackages) + */ + public void addPackage(String packageName, String... excludedPackages) { try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); String path = packageName.replace('.', '/'); Enumeration resources = classLoader.getResources(path); Set classNames = new HashSet<>(); + Set excludedPrefixes = new HashSet<>(); + + // Prepare excluded package prefixes for matching + for (String excluded : excludedPackages) { + if (excluded != null && !excluded.isEmpty()) { + excludedPrefixes.add(excluded + "."); + } + } while (resources.hasMoreElements()) { URL resource = resources.nextElement(); @@ -74,7 +94,7 @@ public void addPackage(String packageName) { if (resource.getProtocol().equals("file")) { // Scan directory File directory = new File(resource.getFile()); - scanDirectory(directory, packageName, classNames); + scanDirectory(directory, packageName, classNames, excludedPrefixes); } else if (resource.getProtocol().equals("jar")) { // Scan JAR file String jarPath = resource.getPath(); @@ -85,7 +105,7 @@ public void addPackage(String packageName) { if (separatorIndex != -1) { jarPath = jarPath.substring(0, separatorIndex); } - scanJar(jarPath, packageName, classNames); + scanJar(jarPath, packageName, classNames, excludedPrefixes); } } @@ -104,10 +124,21 @@ public void addPackage(String packageName) { } private void scanDirectory(File directory, String packageName, Set classNames) { + scanDirectory(directory, packageName, classNames, new HashSet<>()); + } + + private void scanDirectory(File directory, String packageName, Set classNames, Set excludedPrefixes) { if (!directory.exists() || !directory.isDirectory()) { return; } + // Check if this package is excluded + for (String excludedPrefix : excludedPrefixes) { + if ((packageName + ".").startsWith(excludedPrefix) || packageName.equals(excludedPrefix.substring(0, excludedPrefix.length() - 1))) { + return; // Skip excluded package + } + } + File[] files = directory.listFiles(); if (files == null) { return; @@ -118,7 +149,7 @@ private void scanDirectory(File directory, String packageName, Set class if (file.isDirectory()) { // Recursively scan subdirectory - scanDirectory(file, packageName + "." + fileName, classNames); + scanDirectory(file, packageName + "." + fileName, classNames, excludedPrefixes); } else if (fileName.endsWith(".class")) { // Add class name String className = packageName + "." + fileName.substring(0, fileName.length() - 6); @@ -128,6 +159,10 @@ private void scanDirectory(File directory, String packageName, Set class } private void scanJar(String jarPath, String packageName, Set classNames) { + scanJar(jarPath, packageName, classNames, new HashSet<>()); + } + + private void scanJar(String jarPath, String packageName, Set classNames, Set excludedPrefixes) { try { JarFile jarFile = new JarFile(jarPath); Enumeration entries = jarFile.entries(); @@ -142,7 +177,20 @@ private void scanJar(String jarPath, String packageName, Set classNames) String className = entryName .substring(0, entryName.length() - 6) .replace('/', '.'); - classNames.add(className); + + // Check if this class is in an excluded package + boolean excluded = false; + for (String excludedPrefix : excludedPrefixes) { + if ((className + ".").startsWith(excludedPrefix) || + className.startsWith(excludedPrefix.substring(0, excludedPrefix.length() - 1) + ".")) { + excluded = true; + break; + } + } + + if (!excluded) { + classNames.add(className); + } } } From db3a7de9260aa8a589fffb62e3c34b37329761ad Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 18:21:29 +0200 Subject: [PATCH 186/337] Drawn static/final denoters to AutocompleteItems --- .../script/autocomplete/AutocompleteItem.java | 24 +++++++++++++++++++ .../script/autocomplete/AutocompleteMenu.java | 17 +++++++++++++ .../JavaAutocompleteProvider.java | 12 ++-------- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 3aee42085..c049504ce 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -396,6 +396,30 @@ public void addScoreBoost(int boost) { public int getMatchScore() { return matchScore; } public int[] getMatchIndices() { return matchIndices; } + /** + * Check if this item represents a static member. + */ + public boolean isStatic() { + if (sourceData instanceof MethodInfo) { + return ((MethodInfo) sourceData).isStatic(); + } else if (sourceData instanceof FieldInfo) { + return ((FieldInfo) sourceData).isStatic(); + } + return false; + } + + /** + * Check if this item represents a final member. + */ + public boolean isFinal() { + if (sourceData instanceof MethodInfo) { + return ((MethodInfo) sourceData).isFinal(); + } else if (sourceData instanceof FieldInfo) { + return ((FieldInfo) sourceData).isFinal(); + } + return false; + } + /** * Get icon identifier based on kind. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index b73ac42a3..2478ccc05 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -4,6 +4,7 @@ import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.ScaledResolution; +import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import org.lwjgl.opengl.GL11; import java.util.ArrayList; @@ -294,6 +295,22 @@ private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth int textX = itemX; int textY = itemY + (ITEM_HEIGHT - font.FONT_HEIGHT) / 2; + + // Draw modifier text (static/final) if present + boolean isStatic = item.isStatic(); + boolean isFinal = item.isFinal(); + if (isStatic || isFinal) { + GL11.glPushMatrix(); + float scale = 0.5f; + GL11.glScalef(scale, scale, scale); + int col = TokenType.MODIFIER.getHexColor(); + + if (isStatic) + font.drawString("s", (int) (textX / scale), (int) (textY / scale), col); + if (isFinal) + font.drawString("f", (int) (textX / scale), (int) (textY / scale) + 10, col); + GL11.glPopMatrix(); + } // Draw icon String icon = item.getIconId(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 4244d11dd..57ac351d7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -405,16 +405,8 @@ private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, b return; } - // Check if the item is a static member - Object sourceData = item.getSourceData(); - boolean isStatic = false; - - if (sourceData instanceof MethodInfo) { - isStatic = ((MethodInfo) sourceData).isStatic(); - } else if (sourceData instanceof FieldInfo) { - isStatic = ((FieldInfo) sourceData).isStatic(); - } - + boolean isStatic = item.isStatic(); + // Apply penalty to static members in instance context if (isStatic) { int matchScore = item.getMatchScore(); From 6d150a9d22d598ca43031355b600959d220575b3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 18:47:14 +0200 Subject: [PATCH 187/337] Truncated AutocompleteItems text --- .../script/autocomplete/AutocompleteMenu.java | 124 +++++++++++++++++- 1 file changed, 120 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index 2478ccc05..1916f4496 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -318,13 +318,30 @@ private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth font.drawString(icon, textX + (ICON_WIDTH - font.getStringWidth(icon)) / 2, textY, iconColor); textX += ICON_WIDTH; + // Calculate available width for text (leave space for type label) + int availableWidth = itemWidth - (textX - itemX); + if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) { + int typeLabelWidth = font.getStringWidth(item.getTypeLabel()); + availableWidth -= typeLabelWidth + PADDING * 2; + } + + // Determine text color - gray for inherited Object methods + int textColor; + if (item.isInheritedObjectMethod()) { + textColor = DIM_TEXT_COLOR; + } else if (item.isDeprecated()) { + textColor = DIM_TEXT_COLOR; + } else { + textColor = TEXT_COLOR; + } + // Draw name with match highlighting and parameter coloring for methods if (item.getKind() == AutocompleteItem.Kind.METHOD) { - drawMethodName(item.getName(), item.getMatchIndices(), textX, textY, - item.isDeprecated() ? DIM_TEXT_COLOR : TEXT_COLOR); + drawMethodNameTruncated(item.getName(), item.getMatchIndices(), textX, textY, + textColor, availableWidth); } else { - drawHighlightedText(item.getName(), item.getMatchIndices(), textX, textY, - item.isDeprecated() ? DIM_TEXT_COLOR : TEXT_COLOR); + drawHighlightedTextTruncated(item.getName(), item.getMatchIndices(), textX, textY, + textColor, availableWidth); } // Draw type label on the right @@ -358,6 +375,54 @@ private void drawMethodName(String text, int[] matchIndices, int x, int y, int b font.drawString(params, paramX, y, DIM_TEXT_COLOR); } + /** + * Draw method name with parameters colored gray, truncated if too long. + */ + private void drawMethodNameTruncated(String text, int[] matchIndices, int x, int y, int baseColor, int maxWidth) { + // Find the opening parenthesis + int parenIndex = text.indexOf('('); + if (parenIndex == -1) { + // No parameters, just draw normally + drawHighlightedTextTruncated(text, matchIndices, x, y, baseColor, maxWidth); + return; + } + + String methodName = text.substring(0, parenIndex); + String params = text.substring(parenIndex); + + int methodNameWidth = font.getStringWidth(methodName); + int paramsWidth = font.getStringWidth(params); + int totalWidth = methodNameWidth + paramsWidth; + + if (totalWidth <= maxWidth) { + // Fits, draw normally + drawHighlightedText(methodName, matchIndices, x, y, baseColor); + int paramX = x + methodNameWidth; + font.drawString(params, paramX, y, DIM_TEXT_COLOR); + } else { + // Need to truncate + String ellipsis = "..."; + int ellipsisWidth = font.getStringWidth(ellipsis); + + // Always show method name, truncate params if needed + if (methodNameWidth + ellipsisWidth < maxWidth) { + drawHighlightedText(methodName, matchIndices, x, y, baseColor); + int paramX = x + methodNameWidth; + + // Truncate parameters + int availableForParams = maxWidth - methodNameWidth - ellipsisWidth; + String truncatedParams = truncateString(params, availableForParams); + font.drawString(truncatedParams + ellipsis, paramX, y, DIM_TEXT_COLOR); + } else { + // Even method name doesn't fit, truncate it too + int availableForMethod = maxWidth - ellipsisWidth; + String truncatedMethod = truncateString(methodName, availableForMethod); + drawHighlightedText(truncatedMethod, matchIndices, x, y, baseColor); + font.drawString(ellipsis, x + font.getStringWidth(truncatedMethod), y, baseColor); + } + } + } + /** * Draw text with specific characters highlighted. */ @@ -383,6 +448,57 @@ private void drawHighlightedText(String text, int[] matchIndices, int x, int y, } } + /** + * Draw text with highlighting, truncated if too long. + */ + private void drawHighlightedTextTruncated(String text, int[] matchIndices, int x, int y, int baseColor, int maxWidth) { + int textWidth = font.getStringWidth(text); + + if (textWidth <= maxWidth) { + // Fits, draw normally + drawHighlightedText(text, matchIndices, x, y, baseColor); + } else { + // Truncate with ellipsis + String ellipsis = "..."; + int ellipsisWidth = font.getStringWidth(ellipsis); + String truncated = truncateString(text, maxWidth - ellipsisWidth); + + // Adjust match indices for truncated text + int[] adjustedIndices = null; + if (matchIndices != null) { + java.util.List validIndices = new java.util.ArrayList<>(); + for (int idx : matchIndices) { + if (idx < truncated.length()) { + validIndices.add(idx); + } + } + adjustedIndices = new int[validIndices.size()]; + for (int i = 0; i < validIndices.size(); i++) { + adjustedIndices[i] = validIndices.get(i); + } + } + + drawHighlightedText(truncated, adjustedIndices, x, y, baseColor); + font.drawString(ellipsis, x + font.getStringWidth(truncated), y, baseColor); + } + } + + /** + * Truncate a string to fit within the given width. + */ + private String truncateString(String text, int maxWidth) { + if (text.isEmpty()) return text; + + int width = 0; + for (int i = 0; i < text.length(); i++) { + width += font.getStringWidth(String.valueOf(text.charAt(i))); + if (width > maxWidth) { + return text.substring(0, Math.max(0, i)); + } + } + return text; + } + /** * Draw the scrollbar. */ From 34379929a5fedcc68261cde122f8f7b867cff7ac Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 18:52:39 +0200 Subject: [PATCH 188/337] Inherited Object methods deprioritized: Methods from Object class that are NOT overridden are now pushed to the bottom of the autocomplete list --- .../script/autocomplete/AutocompleteItem.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index c049504ce..c51d055ce 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -420,6 +420,49 @@ public boolean isFinal() { return false; } + /** + * Check if this method is inherited from Object class and NOT overridden. + * These methods should be deprioritized in autocomplete. + */ + public boolean isInheritedObjectMethod() { + if (kind != Kind.METHOD || !(sourceData instanceof MethodInfo)) { + return false; + } + + MethodInfo methodInfo = (MethodInfo) sourceData; + + // For Java reflection methods, check the declaring class + if (methodInfo.getJavaMethod() != null) { + java.lang.reflect.Method javaMethod = methodInfo.getJavaMethod(); + Class declaringClass = javaMethod.getDeclaringClass(); + + // If the method is declared in Object, it's an inherited Object method + // unless the containing type is Object itself + if (declaringClass.getName().equals("java.lang.Object")) { + TypeInfo containingType = methodInfo.getContainingType(); + // If we're showing methods for Object itself, don't treat them as "inherited" + return containingType != null && !containingType.getFullName().equals("java.lang.Object"); + } + } else { + // For script-defined methods, use the original logic + TypeInfo containingType = methodInfo.getContainingType(); + + // Check if declaring class is Object + if (containingType != null && containingType.getFullName().equals("java.lang.Object")) { + return true; + } + + // Check if method overrides from Object (meaning it's overridden, so NOT inherited) + if (methodInfo.isOverride()) { + TypeInfo overridesFrom = methodInfo.getOverridesFrom(); + // If it overrides from Object, it means this class overrode it, so it's NOT just inherited + return false; + } + } + + return false; + } + /** * Get icon identifier based on kind. */ @@ -460,6 +503,14 @@ public int compareTo(AutocompleteItem other) { if (this.matchScore != other.matchScore) { return other.matchScore - this.matchScore; } + + // Push inherited Object methods to the bottom + boolean thisIsObjectMethod = this.isInheritedObjectMethod(); + boolean otherIsObjectMethod = other.isInheritedObjectMethod(); + if (thisIsObjectMethod != otherIsObjectMethod) { + return thisIsObjectMethod ? 1 : -1; // Object methods go to bottom + } + // Then by kind priority if (this.kind.getPriority() != other.kind.getPriority()) { return this.kind.getPriority() - other.kind.getPriority(); From 080b41a3b973a10218b5e0e6b07d6a3051080951 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 19:07:17 +0200 Subject: [PATCH 189/337] Accounted for method parameter count in UsageTracker --- .../util/script/autocomplete/AutocompleteItem.java | 9 +++++++++ .../gui/util/script/autocomplete/UsageTracker.java | 14 ++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index c51d055ce..ee7921cec 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -395,6 +395,15 @@ public void addScoreBoost(int boost) { public String getImportPath() { return importPath; } public int getMatchScore() { return matchScore; } public int[] getMatchIndices() { return matchIndices; } + + public int getParameterCount() { + if (sourceData instanceof MethodInfo) { + return ((MethodInfo) sourceData).getParameterCount(); + } else if (sourceData instanceof JSMethodInfo) { + return ((JSMethodInfo) sourceData).getParameterCount(); + } + return 0; + } /** * Check if this item represents a static member. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java index 6ee55bd31..6d23e0c2f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/UsageTracker.java @@ -156,6 +156,13 @@ public void recordUsage(String owner, String name, AutocompleteItem.Kind kind) { */ public void recordUsage(AutocompleteItem item, String ownerFullName) { String name = item.getSearchName() != null ? item.getSearchName() : item.getName(); + + // For methods, include parameter count to distinguish overloads + if (item.getKind() == AutocompleteItem.Kind.METHOD) { + int paramCount = item.getParameterCount(); + name = name + "(" + paramCount + ")"; + } + recordUsage(ownerFullName, name, item.getKind()); } @@ -177,6 +184,13 @@ public int getUsageCount(String owner, String name, AutocompleteItem.Kind kind) */ public int getUsageCount(AutocompleteItem item, String ownerFullName) { String name = item.getSearchName() != null ? item.getSearchName() : item.getName(); + + // For methods, include parameter count to distinguish overloads + if (item.getKind() == AutocompleteItem.Kind.METHOD) { + int paramCount = item.getParameterCount(); + name = name + "(" + paramCount + ")"; + } + return getUsageCount(ownerFullName, name, item.getKind()); } From a36cc044bd483eefe425b76c61cfee06b3c78ffe Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 19:15:32 +0200 Subject: [PATCH 190/337] Shoved object methods all the way down in autocomplete menu --- .../script/autocomplete/AutocompleteItem.java | 10 ++------ .../JavaAutocompleteProvider.java | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index ee7921cec..80ad10f0c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -508,22 +508,16 @@ public int getIconColor() { @Override public int compareTo(AutocompleteItem other) { - // First by match score (descending) + // First by match score (descending) - this includes all penalties and boosts if (this.matchScore != other.matchScore) { return other.matchScore - this.matchScore; } - // Push inherited Object methods to the bottom - boolean thisIsObjectMethod = this.isInheritedObjectMethod(); - boolean otherIsObjectMethod = other.isInheritedObjectMethod(); - if (thisIsObjectMethod != otherIsObjectMethod) { - return thisIsObjectMethod ? 1 : -1; // Object methods go to bottom - } - // Then by kind priority if (this.kind.getPriority() != other.kind.getPriority()) { return this.kind.getPriority() - other.kind.getPriority(); } + // Finally alphabetically return this.name.compareToIgnoreCase(other.name); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 57ac351d7..f7e3f2aa2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -357,6 +357,7 @@ private void filterAndScore(List items, String prefix, item.calculateMatchScore("", false); applyUsageBoost(item, tracker, ownerFullName); applyStaticPenalty(item, isMemberAccess, isStaticContext); + applyObjectMethodPenalty(item); } return; } @@ -365,7 +366,7 @@ private void filterAndScore(List items, String prefix, // For member access (after dot), allow fuzzy/contains matching boolean requirePrefix = !isMemberAccess; - // Filter, score, apply usage boosts, and apply static penalties + // Filter, score, apply usage boosts, and apply penalties Iterator iter = items.iterator(); while (iter.hasNext()) { AutocompleteItem item = iter.next(); @@ -375,6 +376,7 @@ private void filterAndScore(List items, String prefix, } else { applyUsageBoost(item, tracker, ownerFullName); applyStaticPenalty(item, isMemberAccess, isStaticContext); + applyObjectMethodPenalty(item); } } } @@ -422,6 +424,27 @@ private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, b } } + /** + * Apply penalty to inherited Object methods to push them to bottom. + * Strong matches get lighter penalty, weak matches get pushed all the way down. + */ + private void applyObjectMethodPenalty(AutocompleteItem item) { + if (!item.isInheritedObjectMethod()) { + return; + } + + int matchScore = item.getMatchScore(); + // Strong prefix matches (score >= 900) get a moderate penalty + // Everything else gets pushed to the very bottom + if (matchScore >= 900) { + // Strong match - moderate penalty to keep it visible but below normal methods + item.addScoreBoost(-500); + } else { + // Weak match - heavy penalty to push to bottom + item.addScoreBoost(-10000); + } + } + /** * Builder class for AutocompleteItem to handle cases without source data. */ From 0d45d636b886c15fa107e3b9dd534a32e8b2f2bc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 19:41:28 +0200 Subject: [PATCH 191/337] Used correct enum class name --- .../gui/util/script/interpreter/method/MethodCallInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index 247c8b584..86ca3b446 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -311,7 +311,7 @@ public void validate() { // For constructors, check if the type has any constructors at all if (receiverType != null && receiverType.hasConstructors()) { setError(ErrorType.WRONG_ARG_COUNT, - "No constructor in '" + methodName + "' matches " + arguments.size() + " argument(s)"); + "No constructor in '" + receiverType.getSimpleName() + "' matches " + arguments.size() + " argument(s)"); } else { setError(ErrorType.UNRESOLVED_METHOD, "Cannot resolve constructor for '" + methodName + "'"); From c56a8ee7d9a88b2c4bea3d3087ed8e7fe2c5cd44 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 19:41:39 +0200 Subject: [PATCH 192/337] Added ScriptType enums to autocmpletion --- .../util/script/autocomplete/JavaAutocompleteProvider.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index f7e3f2aa2..d4f59926c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.autocomplete; import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; @@ -152,6 +153,11 @@ private void addScriptTypeMembers(ScriptTypeInfo scriptType, List) + for (EnumConstantInfo enumConstant : scriptType.getEnumConstants().values()) { + items.add(AutocompleteItem.fromField(enumConstant.getFieldInfo())); + } + // Add parent class members if (scriptType.hasSuperClass()) { TypeInfo superType = scriptType.getSuperClass(); From 50bcbb5872be0d2d49347888f3295e1cac98b403 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 20:03:25 +0200 Subject: [PATCH 193/337] Bunch of JS fixes --- .../script/autocomplete/AutocompleteManager.java | 8 ++++++-- .../util/script/interpreter/ScriptDocument.java | 16 +++++++++++++--- .../interpreter/js_parser/JSScriptAnalyzer.java | 15 +++++++++++++-- .../interpreter/js_parser/JSTypeRegistry.java | 11 +++++++++-- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 360d0633c..47c05c286 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -1,12 +1,10 @@ package noppes.npcs.client.gui.util.script.autocomplete; -import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import org.lwjgl.input.Mouse; -import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; @@ -391,6 +389,12 @@ private void showSuggestions(String text, int cursorPosition, String prefix, // Get suggestions from appropriate provider AutocompleteProvider provider = document.isJavaScript() ? jsProvider : javaProvider; + + // For JavaScript, update variable types from analyzer + if (document.isJavaScript() && document.getJSAnalyzer() != null) { + jsProvider.updateVariableTypes(document.getJSAnalyzer().getVariableTypes()); + } + List suggestions = provider.getSuggestions(context); // Limit suggestions to prevent overwhelming the UI and improve performance 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 ca037bdc8..af9d60ae1 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 @@ -161,7 +161,7 @@ public String getLanguage() { * Check if this is a JavaScript/ECMAScript document. */ public boolean isJavaScript() { - return false;//"ECMAScript".equalsIgnoreCase(language); + return "ECMAScript".equalsIgnoreCase(language); } // ==================== TEXT MANAGEMENT ==================== @@ -271,14 +271,24 @@ public void formatCodeText() { // Phase 7: Compute indent guides computeIndentGuides(marks); } + + // Store the last JS analyzer for autocomplete + private JSScriptAnalyzer currentJSAnalyzer; /** * Format JavaScript/ECMAScript code using the JS type inference system. */ private List formatJavaScript() { // Use JSScriptAnalyzer for JS-specific analysis - JSScriptAnalyzer analyzer = new JSScriptAnalyzer(text); - return analyzer.analyze(); + currentJSAnalyzer = new JSScriptAnalyzer(this); + return currentJSAnalyzer.analyze(); + } + + /** + * Get the last JS analyzer (for autocomplete to access variable types). + */ + public JSScriptAnalyzer getJSAnalyzer() { + return currentJSAnalyzer; } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java index 6da33a795..103ee677c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java @@ -1,7 +1,9 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import scala.annotation.meta.field; import java.util.*; import java.util.regex.*; @@ -12,6 +14,7 @@ */ public class JSScriptAnalyzer { + private final ScriptDocument document; private final String text; private final JSTypeRegistry registry; @@ -41,8 +44,9 @@ public class JSScriptAnalyzer { private static final Pattern NUMBER_PATTERN = Pattern.compile("\\b\\d+\\.?\\d*\\b"); - public JSScriptAnalyzer(String text) { - this.text = text; + public JSScriptAnalyzer(ScriptDocument document) { + this.document = document; + this.text = document.getText(); this.registry = JSTypeRegistry.getInstance(); // Ensure registry is initialized if (!registry.isInitialized()) { @@ -385,4 +389,11 @@ private void addPatternMarks(List marks, Pattern pattern, Token public String getVariableType(String varName) { return variableTypes.get(varName); } + + /** + * Get all inferred variable types. + */ + public Map getVariableTypes() { + return new HashMap<>(variableTypes); + } } 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 bc6b71688..e9bdf053f 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 @@ -254,12 +254,15 @@ private JSTypeInfo getType(String name, Set visited) { // Detect circular alias references if (visited.contains(baseName)) { - System.err.println("[JSTypeRegistry] Circular type alias detected: " + baseName); + // Circular alias detected - try direct type lookup as fallback + if (types.containsKey(baseName)) { + return types.get(baseName); + } return null; } visited.add(baseName); - // Direct lookup + // Direct lookup in types first (higher priority than aliases) if (types.containsKey(baseName)) { return types.get(baseName); } @@ -267,6 +270,10 @@ private JSTypeInfo getType(String name, Set visited) { // Check type aliases if (typeAliases.containsKey(baseName)) { String resolved = typeAliases.get(baseName); + // Don't follow alias if it resolves to itself + if (resolved.equals(baseName)) { + return null; + } return getType(resolved, visited); } From 61a363fdbac0ac3ae111cedf72ab831f9d987ddf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 20:56:11 +0200 Subject: [PATCH 194/337] A bunch of JS parser fixes and improvements --- .../script/interpreter/field/FieldInfo.java | 72 +++++++ .../interpreter/js_parser/JSHoverInfo.java | 117 ------------ .../js_parser/JSScriptAnalyzer.java | 178 +++++++++++++----- .../interpreter/js_parser/JSTypeRegistry.java | 2 +- .../script/interpreter/method/MethodInfo.java | 81 ++++++++ .../script/interpreter/type/TypeInfo.java | 143 +++++++++++++- 6 files changed, 420 insertions(+), 173 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index fd6890fb0..d10218d83 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -1,5 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter.field; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -142,6 +145,75 @@ public static FieldInfo fromReflection(Field field, TypeInfo containingType) { return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1, field.getModifiers(), field); } + + /** + * Create a FieldInfo from a JSFieldInfo (parsed from .d.ts files). + * Used when resolving field access on JavaScript types. + * + * @param jsField The JavaScript field info from the type registry + * @param containingType The TypeInfo that owns this field + * @return A FieldInfo representing the JavaScript field + */ + public static FieldInfo fromJSField(JSFieldInfo jsField, TypeInfo containingType) { + String name = jsField.getName(); + + // Resolve the type from the JS type registry + TypeInfo type = resolveJSType(jsField.getType()); + + // JS fields are public by default, readonly maps to final + int modifiers = Modifier.PUBLIC; + if (jsField.isReadonly()) { + modifiers |= Modifier.FINAL; + } + + // Use documentation if available + String documentation = jsField.getDocumentation(); + + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null); + } + + /** + * Resolves a JavaScript type name to a TypeInfo. + * Handles primitives, mapped types, and custom types from the registry. + */ + private static TypeInfo resolveJSType(String jsTypeName) { + if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) { + return TypeInfo.fromPrimitive("void"); + } + + // Handle JS primitives + switch (jsTypeName) { + case "string": + return TypeInfo.fromClass(String.class); + case "number": + return TypeInfo.fromClass(double.class); + case "boolean": + return TypeInfo.fromClass(boolean.class); + case "any": + return TypeInfo.fromClass(Object.class); + case "void": + return TypeInfo.fromPrimitive("void"); + } + + // Handle array types + if (jsTypeName.endsWith("[]")) { + String elementType = jsTypeName.substring(0, jsTypeName.length() - 2); + TypeInfo elementTypeInfo = resolveJSType(elementType); + return TypeInfo.arrayOf(elementTypeInfo); + } + + // Try to resolve from the JS type registry + JSTypeRegistry registry = JSTypeRegistry.getInstance(); + if (registry != null) { + JSTypeInfo jsTypeInfo = registry.getType(jsTypeName); + if (jsTypeInfo != null) { + return TypeInfo.fromJSTypeInfo(jsTypeInfo); + } + } + + // Fallback: unresolved type + return TypeInfo.unresolved(jsTypeName, jsTypeName); + } /** * Create a FieldInfo for an enum constant. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java deleted file mode 100644 index edc2879ca..000000000 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSHoverInfo.java +++ /dev/null @@ -1,117 +0,0 @@ -package noppes.npcs.client.gui.util.script.interpreter.js_parser; - -import java.util.List; - -/** - * Container classes for hover information in JS scripts. - */ -public class JSHoverInfo { - - /** - * Hover info for a variable. - */ - public static class VariableInfo { - public final String name; - public final String typeName; - public final JSTypeInfo resolvedType; - - public VariableInfo(String name, String typeName, JSTypeInfo resolvedType) { - this.name = name; - this.typeName = typeName; - this.resolvedType = resolvedType; - } - - public String buildHoverHtml() { - StringBuilder sb = new StringBuilder(); - sb.append("").append(name).append(": ").append(typeName).append(""); - - if (resolvedType != null && resolvedType.getDocumentation() != null) { - sb.append("

").append(resolvedType.getDocumentation()); - } - - return sb.toString(); - } - } - - /** - * Hover info for a method. - */ - public static class MethodInfo { - public final JSMethodInfo method; - public final JSTypeInfo containingType; - - public MethodInfo(JSMethodInfo method, JSTypeInfo containingType) { - this.method = method; - this.containingType = containingType; - } - - public String buildHoverHtml() { - StringBuilder sb = new StringBuilder(); - - // Show containing type - if (containingType != null) { - sb.append("").append(containingType.getFullName()).append("
"); - } - - // Show method signature - sb.append(method.buildHoverInfo()); - - return sb.toString(); - } - } - - /** - * Hover info for a field. - */ - public static class FieldInfo { - public final JSFieldInfo field; - public final JSTypeInfo containingType; - - public FieldInfo(JSFieldInfo field, JSTypeInfo containingType) { - this.field = field; - this.containingType = containingType; - } - - public String buildHoverHtml() { - StringBuilder sb = new StringBuilder(); - - // Show containing type - if (containingType != null) { - sb.append("").append(containingType.getFullName()).append("
"); - } - - // Show field info - sb.append(field.buildHoverInfo()); - - return sb.toString(); - } - } - - /** - * Hover info for a hook function. - */ - public static class FunctionInfo { - public final String name; - public final List signatures; - - public FunctionInfo(String name, List signatures) { - this.name = name; - this.signatures = signatures; - } - - public String buildHoverHtml() { - StringBuilder sb = new StringBuilder(); - sb.append("Hook: ").append(name).append(""); - - if (!signatures.isEmpty()) { - sb.append("

Overloads:"); - for (JSTypeRegistry.HookSignature sig : signatures) { - sb.append("
• function ").append(name).append("(") - .append(sig.paramName).append(": ").append(sig.paramType).append(")"); - } - } - - return sb.toString(); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java index 103ee677c..0f117c4d4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java @@ -2,7 +2,10 @@ import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import scala.annotation.meta.field; import java.util.*; @@ -127,32 +130,48 @@ private void parseFunctions(List marks) { // Check if this is a known hook if (registry.isHook(funcName)) { - marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, - new JSHoverInfo.FunctionInfo(funcName, registry.getHookSignatures(funcName)))); - - // Infer parameter types from hook + // Create a unified MethodInfo for the hook List sigs = registry.getHookSignatures(funcName); - if (!sigs.isEmpty() && !params.isEmpty()) { - // Parse parameter names from function - String[] paramNames = params.split(","); - if (paramNames.length > 0) { - String paramName = paramNames[0].trim(); - // Use first hook signature's type - String paramType = sigs.get(0).paramType; - - // Store in function params and variable types - Map funcParamMap = new HashMap<>(); - funcParamMap.put(paramName, paramType); - functionParams.put(funcName, funcParamMap); - variableTypes.put(paramName, paramType); - - // Mark the parameter - int paramStart = m.start(2) + params.indexOf(paramName); - int paramEnd = paramStart + paramName.length(); - JSTypeInfo typeInfo = registry.getType(paramType); - marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER, - new JSHoverInfo.VariableInfo(paramName, paramType, typeInfo))); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + + // Create unified MethodInfo for hover info + TypeInfo paramTypeInfo = resolveJSType(sig.paramType); + List methodParams = new ArrayList<>(); + methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo)); + + MethodInfo hookMethod = MethodInfo.declaration( + funcName, null, TypeInfo.fromPrimitive("void"), methodParams, + nameStart, nameStart, nameStart, -1, -1, 0, sig.doc + ); + + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, hookMethod)); + + // Infer parameter types from hook + if (!params.isEmpty()) { + // Parse parameter names from function + String[] paramNames = params.split(","); + if (paramNames.length > 0) { + String paramName = paramNames[0].trim(); + String paramType = sig.paramType; + + // Store in function params and variable types + Map funcParamMap = new HashMap<>(); + funcParamMap.put(paramName, paramType); + functionParams.put(funcName, funcParamMap); + variableTypes.put(paramName, paramType); + + // Mark the parameter with unified FieldInfo + int paramStart = m.start(2) + params.indexOf(paramName); + int paramEnd = paramStart + paramName.length(); + FieldInfo paramFieldInfo = FieldInfo.parameter( + paramName, paramTypeInfo, paramStart, hookMethod + ); + marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER, paramFieldInfo)); + } } + } else { + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName)); } } else { marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName)); @@ -181,12 +200,12 @@ private void parseVariables(List marks) { variableTypes.put(varName, inferredType); } - // Mark variable declaration + // Mark variable declaration with unified FieldInfo int varStart = m.start(1); int varEnd = m.end(1); - JSTypeInfo typeInfo = inferredType != null ? registry.getType(inferredType) : null; - marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, - new JSHoverInfo.VariableInfo(varName, inferredType != null ? inferredType : "any", typeInfo))); + TypeInfo typeInfo = resolveJSType(inferredType != null ? inferredType : "any"); + FieldInfo varFieldInfo = FieldInfo.localField(varName, typeInfo, varStart, null); + marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, varFieldInfo)); } } @@ -270,9 +289,9 @@ private String inferTypeFromMethodCall(String expr) { } // Check if it's a field - JSFieldInfo field = typeInfo.getField(member); - if (field != null) { - currentType = field.getType(); + JSFieldInfo f = typeInfo.getField(member); + if (f != null) { + currentType = f.getType(); continue; } @@ -301,11 +320,11 @@ private void markMemberAccesses(List marks) { int pos = m.start(); - // Mark receiver + // Mark receiver with unified FieldInfo if (currentType != null) { - JSTypeInfo typeInfo = registry.getType(currentType); - marks.add(new ScriptLine.Mark(pos, pos + receiverName.length(), TokenType.LOCAL_FIELD, - new JSHoverInfo.VariableInfo(receiverName, currentType, typeInfo))); + TypeInfo unifiedType = resolveJSType(currentType); + FieldInfo receiverField = FieldInfo.localField(receiverName, unifiedType, pos, null); + marks.add(new ScriptLine.Mark(pos, pos + receiverName.length(), TokenType.LOCAL_FIELD, receiverField)); } pos += receiverName.length() + 1; // +1 for the dot @@ -317,25 +336,29 @@ private void markMemberAccesses(List marks) { int memberEnd = pos + member.length(); if (currentType != null) { - JSTypeInfo typeInfo = registry.getType(currentType); - if (typeInfo != null) { + JSTypeInfo jsTypeInfo = registry.getType(currentType); + if (jsTypeInfo != null) { + TypeInfo unifiedContainingType = TypeInfo.fromJSTypeInfo(jsTypeInfo); + // Check method first - JSMethodInfo method = typeInfo.getMethod(member); - if (method != null) { - marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.METHOD_CALL, - new JSHoverInfo.MethodInfo(method, typeInfo))); - currentType = method.getReturnType(); + JSMethodInfo jsMethod = jsTypeInfo.getMethod(member); + if (jsMethod != null) { + // Convert to unified MethodInfo + MethodInfo unifiedMethod = MethodInfo.fromJSMethod(jsMethod, unifiedContainingType); + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.METHOD_CALL, unifiedMethod)); + currentType = jsMethod.getReturnType(); } else { // Check field - JSFieldInfo field = typeInfo.getField(member); - if (field != null) { - marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.GLOBAL_FIELD, - new JSHoverInfo.FieldInfo(field, typeInfo))); - currentType = field.getType(); + JSFieldInfo jsField = jsTypeInfo.getField(member); + if (jsField != null) { + // Convert to unified FieldInfo + FieldInfo unifiedField = FieldInfo.fromJSField(jsField, unifiedContainingType); + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.GLOBAL_FIELD, unifiedField)); + currentType = jsField.getType(); } else { // Unknown member - mark as undefined marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.UNDEFINED_VAR, - "Unknown member '" + member + "' on type " + typeInfo.getFullName())); + "Unknown member '" + member + "' on type " + jsTypeInfo.getFullName())); currentType = null; } } @@ -364,9 +387,20 @@ private void markMethodCalls(List marks) { int nameEnd = m.end(1); if (registry.isHook(callExpr)) { - // It's a known hook being called - marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, - new JSHoverInfo.FunctionInfo(callExpr, registry.getHookSignatures(callExpr)))); + // It's a known hook being called - create unified MethodInfo + List sigs = registry.getHookSignatures(callExpr); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + TypeInfo paramTypeInfo = resolveJSType(sig.paramType); + List methodParams = new ArrayList<>(); + methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo)); + + MethodInfo hookMethod = MethodInfo.declaration( + callExpr, null, TypeInfo.fromPrimitive("void"), methodParams, + nameStart, nameStart, nameStart, -1, -1, 0, sig.doc + ); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, hookMethod)); + } } } // Chained calls are handled in markMemberAccesses @@ -396,4 +430,46 @@ public String getVariableType(String varName) { public Map getVariableTypes() { return new HashMap<>(variableTypes); } + + /** + * Resolves a JavaScript type name to a unified TypeInfo. + * Handles primitives, mapped types, and custom types from the registry. + */ + private TypeInfo resolveJSType(String jsTypeName) { + if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) { + return TypeInfo.fromPrimitive("void"); + } + + // Handle JS primitives + switch (jsTypeName) { + case "string": + return TypeInfo.fromClass(String.class); + case "number": + return TypeInfo.fromClass(double.class); + case "boolean": + return TypeInfo.fromClass(boolean.class); + case "any": + return TypeInfo.fromClass(Object.class); + case "void": + return TypeInfo.fromPrimitive("void"); + } + + // Handle array types + if (jsTypeName.endsWith("[]")) { + String elementType = jsTypeName.substring(0, jsTypeName.length() - 2); + TypeInfo elementTypeInfo = resolveJSType(elementType); + return TypeInfo.arrayOf(elementTypeInfo); + } + + // Try to resolve from the JS type registry + if (registry != null) { + JSTypeInfo jsTypeInfo = registry.getType(jsTypeName); + if (jsTypeInfo != null) { + return TypeInfo.fromJSTypeInfo(jsTypeInfo); + } + } + + // Fallback: unresolved type + return TypeInfo.unresolved(jsTypeName, jsTypeName); + } } 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 e9bdf053f..741543fa2 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 @@ -94,7 +94,7 @@ private void loadResourceFile(TypeScriptDefinitionParser parser, String fileName sb.append(line).append("\n"); } parser.parseDefinitionFile(sb.toString(), fileName); - } + } } } catch (Exception e) { // File might not exist, that's ok diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index f39875b26..b5b1d9b7a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -2,6 +2,9 @@ import noppes.npcs.client.gui.util.script.interpreter.*; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -160,6 +163,84 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, true, modifiers, null, null); } + /** + * Create a MethodInfo from a JSMethodInfo (parsed from .d.ts files). + * Used when resolving method calls on JavaScript types. + * + * @param jsMethod The JavaScript method info from the type registry + * @param containingType The TypeInfo that owns this method + * @return A MethodInfo representing the JavaScript method + */ + public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containingType) { + String name = jsMethod.getName(); + + // Resolve the return type from the JS type registry + TypeInfo returnType = resolveJSType(jsMethod.getReturnType()); + + // Convert JS parameters to FieldInfo + List params = new ArrayList<>(); + List jsParams = jsMethod.getParameters(); + + for (JSMethodInfo.JSParameterInfo param : jsParams) { + String paramName = param.getName(); + String paramTypeName = param.getType(); + TypeInfo paramType = resolveJSType(paramTypeName); + params.add(FieldInfo.reflectionParam(paramName, paramType)); + } + + // JS methods are always public (no access modifiers in .d.ts) + int modifiers = Modifier.PUBLIC; + + // Use the documentation from the method if available + String documentation = jsMethod.getDocumentation(); + + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, null); + } + + /** + * Resolves a JavaScript type name to a TypeInfo. + * Handles primitives, mapped types, and custom types from the registry. + */ + private static TypeInfo resolveJSType(String jsTypeName) { + if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) { + return TypeInfo.fromPrimitive("void"); + } + + // Handle JS primitives + switch (jsTypeName) { + case "string": + return TypeInfo.fromClass(String.class); + case "number": + return TypeInfo.fromClass(double.class); + case "boolean": + return TypeInfo.fromClass(boolean.class); + case "any": + return TypeInfo.fromClass(Object.class); + case "void": + return TypeInfo.fromPrimitive("void"); + } + + // Handle array types + if (jsTypeName.endsWith("[]")) { + String elementType = jsTypeName.substring(0, jsTypeName.length() - 2); + TypeInfo elementTypeInfo = resolveJSType(elementType); + // For arrays, we create an array type representation + return TypeInfo.arrayOf(elementTypeInfo); + } + + // Try to resolve from the JS type registry + JSTypeRegistry registry = JSTypeRegistry.getInstance(); + if (registry != null) { + JSTypeInfo jsTypeInfo = registry.getType(jsTypeName); + if (jsTypeInfo != null) { + return TypeInfo.fromJSTypeInfo(jsTypeInfo); + } + } + + // Fallback: unresolved type + return TypeInfo.unresolved(jsTypeName, jsTypeName); + } + // Getters public String getName() { return name; } public TypeInfo getReturnType() { return returnType; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index c49a095bf..cf4dbfd11 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -2,6 +2,9 @@ import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; @@ -11,7 +14,14 @@ /** * Represents resolved type information for a class/interface/enum. - * Base class for type metadata. Extended by ScriptTypeInfo for script-defined types. + * + * This is the unified type system that supports: + * - Java types (via Class reflection) + * - Script-defined types (via ScriptTypeInfo subclass) + * - JavaScript/TypeScript types (via JSTypeInfo bridge) + * + * For JavaScript types, the jsTypeInfo field holds the parsed .d.ts data, + * and methods like hasMethod/getMethodInfo delegate to it. */ public class TypeInfo { @@ -21,17 +31,31 @@ public enum Kind { ENUM, UNKNOWN } + + /** + * Singleton constant for the void type. + */ + public static final TypeInfo VOID = fromPrimitive("void"); private final String simpleName; // e.g., "List", "ColorType" private final String fullName; // e.g., "java.util.List", "kamkeel...IOverlay$ColorType" private final String packageName; // e.g., "java.util", "kamkeel.npcdbc.api.client.overlay" private final Kind kind; // CLASS, INTERFACE, ENUM - private final Class javaClass; // The actual resolved Java class (null if unresolved) + private final Class javaClass; // The actual resolved Java class (null if unresolved or JS type) private final boolean resolved; // Whether this type was successfully resolved private final TypeInfo enclosingType; // For inner classes, the outer type (null if top-level) + + // JavaScript/TypeScript type info (for types from .d.ts files) + private final JSTypeInfo jsTypeInfo; // The JS type info (null if Java type) private TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { + this(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, null); + } + + private TypeInfo(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, + JSTypeInfo jsTypeInfo) { this.simpleName = simpleName; this.fullName = fullName; this.packageName = packageName; @@ -39,6 +63,7 @@ private TypeInfo(String simpleName, String fullName, String packageName, this.javaClass = javaClass; this.resolved = resolved; this.enclosingType = enclosingType; + this.jsTypeInfo = jsTypeInfo; } // Protected constructor for subclasses (like ScriptTypeInfo) @@ -52,6 +77,7 @@ protected TypeInfo(String simpleName, String fullName, String packageName, this.javaClass = javaClass; this.resolved = resolved; this.enclosingType = enclosingType; + this.jsTypeInfo = null; } // Factory methods @@ -122,6 +148,67 @@ public static TypeInfo fromPrimitive(String typeName) { } return new TypeInfo(typeName, typeName, "", Kind.CLASS, primitiveClass, true, null); } + + /** + * Create a TypeInfo from a JavaScript/TypeScript type. + * This bridges JS types parsed from .d.ts files into the unified type system. + * + * @param jsType The parsed JS type info + * @return A TypeInfo wrapping the JS type + */ + public static TypeInfo fromJSTypeInfo(JSTypeInfo jsType) { + if (jsType == null) return null; + + String simpleName = jsType.getSimpleName(); + String fullName = jsType.getFullName(); + String namespace = jsType.getNamespace(); + + // JS interfaces are always Kind.INTERFACE + return new TypeInfo(simpleName, fullName, namespace != null ? namespace : "", + Kind.INTERFACE, null, true, null, jsType); + } + + /** + * Create an array type wrapping the given element type. + * + * @param elementType The type of elements in the array + * @return A TypeInfo representing the array type + */ + public static TypeInfo arrayOf(TypeInfo elementType) { + if (elementType == null) { + return fromClass(Object[].class); + } + + String simpleName = elementType.getSimpleName() + "[]"; + String fullName = elementType.getFullName() + "[]"; + String pkg = elementType.getPackageName(); + + // Try to get the actual array class if we have a Java class + Class arrayClass = null; + if (elementType.getJavaClass() != null) { + try { + arrayClass = java.lang.reflect.Array.newInstance(elementType.getJavaClass(), 0).getClass(); + } catch (Exception e) { + // Fallback to Object array if we can't create the specific array type + } + } + + return new TypeInfo(simpleName, fullName, pkg, Kind.CLASS, arrayClass, true, null); + } + + /** + * Check if this is a JavaScript type (backed by JSTypeInfo). + */ + public boolean isJSType() { + return jsTypeInfo != null; + } + + /** + * Get the underlying JSTypeInfo if this is a JS type. + */ + public JSTypeInfo getJSTypeInfo() { + return jsTypeInfo; + } // Getters public String getSimpleName() { return simpleName; } @@ -160,6 +247,11 @@ public TokenType getTokenType() { * Check if this type has a method with the given name. */ public boolean hasMethod(String methodName) { + // Check JS type first + if (jsTypeInfo != null) { + return jsTypeInfo.hasMethod(methodName); + } + if (javaClass == null) return false; try { for (java.lang.reflect.Method m : javaClass.getMethods()) { @@ -177,6 +269,17 @@ public boolean hasMethod(String methodName) { * Check if this type has a method with the given name and parameter count. */ public boolean hasMethod(String methodName, int paramCount) { + // Check JS type first + if (jsTypeInfo != null) { + List overloads = jsTypeInfo.getMethodOverloads(methodName); + for (JSMethodInfo m : overloads) { + if (m.getParameterCount() == paramCount) { + return true; + } + } + return false; + } + if (javaClass == null) return false; try { for (java.lang.reflect.Method m : javaClass.getMethods()) { @@ -274,6 +377,11 @@ public MethodInfo findConstructor(TypeInfo[] argTypes) { * Check if this type has a field with the given name. */ public boolean hasField(String fieldName) { + // Check JS type first + if (jsTypeInfo != null) { + return jsTypeInfo.hasField(fieldName); + } + if (javaClass == null) return false; try { for (java.lang.reflect.Field f : javaClass.getFields()) { @@ -320,9 +428,18 @@ public EnumConstantInfo getEnumConstant(String constantName) { /** * Get MethodInfo for a method by name. Returns null if not found. - * Creates a synthetic MethodInfo based on reflection data. + * Creates a synthetic MethodInfo based on reflection data or JS type data. */ public MethodInfo getMethodInfo(String methodName) { + // Check JS type first + if (jsTypeInfo != null) { + JSMethodInfo jsMethod = jsTypeInfo.getMethod(methodName); + if (jsMethod != null) { + return MethodInfo.fromJSMethod(jsMethod, this); + } + return null; + } + if (javaClass == null) return null; try { for (java.lang.reflect.Method m : javaClass.getMethods()) { @@ -343,6 +460,15 @@ public MethodInfo getMethodInfo(String methodName) { */ public java.util.List getAllMethodOverloads(String methodName) { java.util.List overloads = new java.util.ArrayList<>(); + + // Check JS type first + if (jsTypeInfo != null) { + for (JSMethodInfo jsMethod : jsTypeInfo.getMethodOverloads(methodName)) { + overloads.add(MethodInfo.fromJSMethod(jsMethod, this)); + } + return overloads; + } + if (javaClass == null) return overloads; try { for (java.lang.reflect.Method m : javaClass.getMethods()) { @@ -507,9 +633,18 @@ public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) /** * Get FieldInfo for a field by name. Returns null if not found. - * Creates a synthetic FieldInfo based on reflection data. + * Creates a synthetic FieldInfo based on reflection data or JS type data. */ public FieldInfo getFieldInfo(String fieldName) { + // Check JS type first + if (jsTypeInfo != null) { + JSFieldInfo jsField = jsTypeInfo.getField(fieldName); + if (jsField != null) { + return FieldInfo.fromJSField(jsField, this); + } + return null; + } + if (javaClass == null) return null; try { for (java.lang.reflect.Field f : javaClass.getFields()) { From b3d49945f75b00320f87a8b4e878a86f5ad09d0c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 21:06:55 +0200 Subject: [PATCH 195/337] Fixed broken .d.ts parsing pattern, bec files like IPlayer.d.ts weren't being parsed at all --- .../js_parser/TypeScriptDefinitionParser.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 726672b47..e898e6806 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 @@ -13,7 +13,7 @@ public class TypeScriptDefinitionParser { // Patterns for parsing .d.ts content private static final Pattern INTERFACE_PATTERN = Pattern.compile( - "export\\s+interface\\s+(\\w+)(?:\\s+extends\\s+([\\w.]+))?\\s*\\{"); + "export\\s+interface\\s+(\\w+)(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); private static final Pattern NAMESPACE_PATTERN = Pattern.compile( "export\\s+namespace\\s+(\\w+)\\s*\\{"); @@ -155,10 +155,19 @@ private void parseInterfaceFile(String content, String parentNamespace) { Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(content); while (interfaceMatcher.find()) { String interfaceName = interfaceMatcher.group(1); - String extendsType = interfaceMatcher.group(2); + String extendsClause = interfaceMatcher.group(2); JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace); - if (extendsType != null) { + if (extendsClause != null) { + // Handle multiple extends (e.g., "IEntityLivingBase, IAnimatable") + // Take the first one, stripping generics + String extendsType = extendsClause.trim(); + // Remove generic parameters like + extendsType = extendsType.replaceAll("<[^>]*>", ""); + // If multiple types (comma-separated), take the first one + if (extendsType.contains(",")) { + extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim(); + } typeInfo.setExtends(extendsType); } From f9babf040d15e2c1f287985ba6ec342f893f3fa2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 21:41:39 +0200 Subject: [PATCH 196/337] temp JS/java merger --- .../script/interpreter/ScriptDocument.java | 731 ++++++++++++++++-- .../js_parser/JSScriptAnalyzer.java | 77 ++ .../interpreter/js_parser/JSTypeRegistry.java | 10 + .../script/interpreter/type/TypeResolver.java | 116 ++- 4 files changed, 870 insertions(+), 64 deletions(-) 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 af9d60ae1..e004fb2e4 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 @@ -8,7 +8,11 @@ import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSScriptAnalyzer; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; @@ -20,6 +24,7 @@ import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; +import scala.annotation.meta.field; import java.util.*; import java.util.regex.Matcher; @@ -233,13 +238,20 @@ public void init(int width, int height) { } // ==================== TOKENIZATION ==================== + + // JS-specific: Function parameter types (funcName -> paramName -> typeName) + private final Map> jsFunctionParams = new HashMap<>(); + + // JS-specific: Inferred variable types (varName -> typeName) + private final Map jsVariableTypes = new HashMap<>(); /** * Main tokenization entry point. * Performs complete analysis and builds tokens for all lines. + * Uses a UNIFIED pipeline for both Java and JavaScript. */ public void formatCodeText() { - // Clear previous state + // Clear previous state (common for both languages) imports.clear(); methods.clear(); globalFields.clear(); @@ -250,15 +262,11 @@ public void formatCodeText() { methodCalls.clear(); externalFieldAssignments.clear(); declarationErrors.clear(); + jsFunctionParams.clear(); + jsVariableTypes.clear(); - List marks; - - // Use different analysis paths for JavaScript vs Java - if (isJavaScript()) { - marks = formatJavaScript(); - } else { - marks = formatJava(); - } + // Unified pipeline for both languages + List marks = formatUnified(); // Phase 5: Resolve conflicts and sort marks = resolveConflicts(marks); @@ -272,41 +280,67 @@ public void formatCodeText() { computeIndentGuides(marks); } - // Store the last JS analyzer for autocomplete + // Store the last JS analyzer for autocomplete (deprecated - use getJSVariableTypes instead) + @Deprecated private JSScriptAnalyzer currentJSAnalyzer; /** - * Format JavaScript/ECMAScript code using the JS type inference system. + * @deprecated Use the unified pipeline. Variable types are now in jsVariableTypes. */ - private List formatJavaScript() { - // Use JSScriptAnalyzer for JS-specific analysis - currentJSAnalyzer = new JSScriptAnalyzer(this); - return currentJSAnalyzer.analyze(); + @Deprecated + public JSScriptAnalyzer getJSAnalyzer() { + return currentJSAnalyzer; } /** - * Get the last JS analyzer (for autocomplete to access variable types). + * Get inferred JS variable types (for autocomplete). */ - public JSScriptAnalyzer getJSAnalyzer() { - return currentJSAnalyzer; + public Map getJSVariableTypes() { + return new HashMap<>(jsVariableTypes); } /** - * Format Java/Groovy code using the full Java analysis system. + * Get JS function parameter types. */ - private List formatJava() { - // Phase 1: Find excluded regions (strings/comments) + public Map> getJSFunctionParams() { + return new HashMap<>(jsFunctionParams); + } + + /** + * Unified format method - single pipeline for both Java and JavaScript. + * The methods called here are language-aware and handle both cases. + */ + private List formatUnified() { + // Phase 1: Find excluded regions (strings/comments) - same for both findExcludedRanges(); - // Phase 2: Parse imports + // Phase 2: Parse imports (Java only, JS skips this) parseImports(); - // Phase 3: Parse structure (script types, methods, fields, locals) + // Phase 3: Parse structure (methods, fields, locals) - language aware parseStructure(); - // Phase 4: Build marks and assign to lines + // Phase 4: Build marks - language aware return buildMarks(); } + + /** + * @deprecated Use formatUnified() instead. Kept for compatibility. + */ + @Deprecated + private List formatJavaScript() { + // Delegate to unified pipeline + currentJSAnalyzer = new JSScriptAnalyzer(this); + return currentJSAnalyzer.analyze(); + } + + /** + * @deprecated Use formatUnified() instead. Kept for compatibility. + */ + @Deprecated + private List formatJava() { + return formatUnified(); + } // ==================== PHASE 1: EXCLUDED RANGES ==================== @@ -377,6 +411,11 @@ private boolean isInCommentRange(int position) { // ==================== PHASE 2: IMPORTS ==================== private void parseImports() { + // JavaScript doesn't use Java-style imports + if (isJavaScript()) { + return; + } + Matcher m = IMPORT_PATTERN.matcher(text); while (m.find()) { if (isExcluded(m.start())) @@ -418,28 +457,37 @@ private void parseStructure() { imp.clearReferences(); } - // Parse script-defined types (classes, interfaces, enums) - parseScriptTypes(); + // Parse script-defined types (classes, interfaces, enums) - Java only + if (!isJavaScript()) { + parseScriptTypes(); + } - // Parse methods + // Parse methods/functions - language aware parseMethodDeclarations(); - // Parse local variables inside methods + // Parse local variables inside methods/functions - language aware parseLocalVariables(); - // Parse global fields (outside methods) - parseGlobalFields(); + // Parse global fields (outside methods) - Java only + if (!isJavaScript()) { + parseGlobalFields(); + } - // Parse and validate assignments (reassignments) - stores in FieldInfo - parseAssignments(); + // Parse and validate assignments (reassignments) - Java only + if (!isJavaScript()) { + parseAssignments(); + } - // Detect method overrides and interface implementations for script types - detectMethodInheritance(); + // Detect method overrides and interface implementations for script types - Java only + if (!isJavaScript()) { + detectMethodInheritance(); + } } /** * Parse class, interface, and enum declarations defined in the script. * Creates ScriptTypeInfo instances and stores them for later resolution. + * Java only - JavaScript doesn't use class declarations in the same way. */ private void parseScriptTypes() { // Pattern: [modifiers] (class|interface|enum) ClassName [optional ()] [extends Parent] [implements I1, I2...] { ... } @@ -626,6 +674,97 @@ private boolean isInsideNestedMethod(int position, int classBodyStart, int class } private void parseMethodDeclarations() { + if (isJavaScript()) { + parseJSFunctions(); + } else { + parseJavaMethods(); + } + } + + /** + * Parse JavaScript function declarations and infer parameter types from hooks. + */ + private void parseJSFunctions() { + // Pattern: function funcName(params) { ... } + Pattern funcPattern = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*\\{"); + Matcher m = funcPattern.matcher(text); + + while (m.find()) { + if (isExcluded(m.start())) continue; + + String funcName = m.group(1); + String paramList = m.group(2); + + int nameStart = m.start(1); + int nameEnd = m.end(1); + int bodyStart = text.indexOf('{', m.end() - 1); + int bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) bodyEnd = text.length(); + + // Check if this is a known hook function + List params = new ArrayList<>(); + TypeInfo returnType = TypeInfo.fromPrimitive("void"); + String documentation = null; + + if (typeResolver.isJSHook(funcName)) { + List sigs = typeResolver.getJSHookSignatures(funcName); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + documentation = sig.doc; + + // Infer parameter types from hook signature + if (paramList != null && !paramList.trim().isEmpty()) { + String[] paramNames = paramList.split(","); + if (paramNames.length > 0) { + String paramName = paramNames[0].trim(); + String paramTypeName = sig.paramType; + TypeInfo paramType = typeResolver.resolveJSType(paramTypeName); + + // Store in tracking maps for later use + Map funcParams = new HashMap<>(); + funcParams.put(paramName, paramTypeName); + jsFunctionParams.put(funcName, funcParams); + jsVariableTypes.put(paramName, paramTypeName); + + // Create parameter with proper type + int paramStart = m.start(2) + paramList.indexOf(paramName); + params.add(FieldInfo.parameter(paramName, paramType, paramStart, null)); + } + } + } + } else { + // Non-hook function - parameters have no type + if (paramList != null && !paramList.trim().isEmpty()) { + int paramOffset = m.start(2); + for (String p : paramList.split(",")) { + String pn = p.trim(); + if (!pn.isEmpty()) { + int paramStart = paramOffset + paramList.indexOf(pn); + params.add(FieldInfo.parameter(pn, TypeInfo.fromPrimitive("any"), paramStart, null)); + + // Track as untyped parameter + Map funcParams = jsFunctionParams.computeIfAbsent(funcName, k -> new HashMap<>()); + funcParams.put(pn, "any"); + } + } + } + } + + // Create MethodInfo for the function + MethodInfo methodInfo = MethodInfo.declaration( + funcName, null, returnType, params, + m.start(), nameStart, nameStart, + bodyStart, bodyEnd, 0, documentation + ); + + methods.add(methodInfo); + } + } + + /** + * Parse Java method declarations. + */ + private void parseJavaMethods() { Pattern methodWithBody = Pattern.compile( "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*(\\{|;)"); @@ -699,25 +838,6 @@ private void parseMethodDeclarations() { // Check for duplicate method declarations checkDuplicateMethods(); - - // Also parse JS-style functions - Matcher funcM = FUNC_PARAMS_PATTERN.matcher(text); - while (funcM.find()) { - if (isExcluded(funcM.start())) - continue; - // JS functions don't have explicit return types - String paramList = funcM.group(1); - List params = new ArrayList<>(); - if (paramList != null && !paramList.trim().isEmpty()) { - for (String p : paramList.split(",")) { - String pn = p.trim(); - if (!pn.isEmpty()) { - params.add(FieldInfo.parameter(pn, null, funcM.start(), null)); - } - } - } - // We don't need to store JS functions in methods list - just extract params - } } /** @@ -876,6 +996,144 @@ private List parseParametersWithPositions(String paramList, int param } private void parseLocalVariables() { + if (isJavaScript()) { + parseJSVariables(); + } else { + parseJavaLocalVariables(); + } + } + + /** + * Parse JavaScript variable declarations (var/let/const). + */ + private void parseJSVariables() { + // Pattern: var/let/const varName = expression; or var/let/const varName; + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;]+))?"); + Matcher m = varPattern.matcher(text); + + while (m.find()) { + if (isExcluded(m.start())) continue; + + String varName = m.group(1); + String initializer = m.group(2); + + int varStart = m.start(1); + int varEnd = m.end(1); + + // Infer type from initializer + String inferredType = "any"; + if (initializer != null) { + inferredType = inferJSTypeFromExpression(initializer.trim()); + } + + // Store in tracking maps + jsVariableTypes.put(varName, inferredType); + + TypeInfo typeInfo = typeResolver.resolveJSType(inferredType); + + // Store as a global field (JS doesn't have method-scoped locals in the same way) + FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, varStart, null); + globalFields.put(varName, fieldInfo); + } + } + + /** + * Infer type from a JavaScript expression. + */ + private String inferJSTypeFromExpression(String expr) { + if (expr == null || expr.isEmpty()) return "any"; + + // String literal + if (expr.startsWith("\"") || expr.startsWith("'")) { + return "string"; + } + + // Number literal + if (expr.matches("-?\\d+\\.?\\d*")) { + return "number"; + } + + // Boolean literal + if (expr.equals("true") || expr.equals("false")) { + return "boolean"; + } + + // Null/undefined + if (expr.equals("null")) return "null"; + if (expr.equals("undefined")) return "undefined"; + + // Array literal + if (expr.startsWith("[")) { + return "any[]"; + } + + // Object literal + if (expr.startsWith("{")) { + return "object"; + } + + // Method call chain: something.method() - infer from method return type + if (expr.contains(".") && expr.contains("(")) { + return inferJSTypeFromMethodCall(expr); + } + + // Variable reference + if (jsVariableTypes.containsKey(expr)) { + return jsVariableTypes.get(expr); + } + + return "any"; + } + + /** + * Infer type from a JavaScript method call chain. + */ + private String inferJSTypeFromMethodCall(String expr) { + // Remove trailing parentheses and args for analysis + int parenIndex = expr.indexOf('('); + if (parenIndex > 0) { + expr = expr.substring(0, parenIndex); + } + + String[] parts = expr.split("\\."); + if (parts.length < 2) return "any"; + + // Start with the receiver type + String currentType = jsVariableTypes.get(parts[0]); + if (currentType == null) return "any"; + + // Walk the chain + JSTypeRegistry registry = typeResolver.getJSTypeRegistry(); + for (int i = 1; i < parts.length; i++) { + JSTypeInfo typeInfo = registry.getType(currentType); + if (typeInfo == null) return "any"; + + String member = parts[i]; + + // Check if it's a method + JSMethodInfo method = typeInfo.getMethod(member); + if (method != null) { + currentType = method.getReturnType(); + continue; + } + + // Check if it's a field + JSFieldInfo field = typeInfo.getField(member); + if (field != null) { + currentType = field.getType(); + continue; + } + + return "any"; // Unknown member + } + + return currentType; + } + + /** + * Parse Java local variables inside methods. + */ + private void parseJavaLocalVariables() { for (MethodInfo method : getAllMethods()) { Map locals = new HashMap<>(); methodLocals.put(method.getDeclarationOffset(), locals); @@ -1450,12 +1708,28 @@ private String cleanDocumentation(String comment) { } public TypeInfo resolveType(String typeName) { + // Use language-appropriate resolution + if (isJavaScript()) { + return resolveJSTypeUnified(typeName); + } return resolveTypeAndTrackUsage(typeName); } + /** + * Resolve a JS type name to unified TypeInfo. + * Handles JS primitives, .d.ts defined types, and falls back to Java types. + */ + private TypeInfo resolveJSTypeUnified(String typeName) { + if (typeName == null || typeName.isEmpty()) { + return TypeInfo.fromPrimitive("void"); + } + return typeResolver.resolveJSType(typeName); + } + /** * Resolve a type and track the import usage for unused import detection. * Checks script-defined types first, then falls back to imported types. + * Used for Java/Groovy scripts. */ private TypeInfo resolveTypeAndTrackUsage(String typeName) { if (typeName == null || typeName.isEmpty()) @@ -1514,10 +1788,311 @@ private TypeInfo resolveTypeAndTrackUsage(String typeName) { private List buildMarks() { List marks = new ArrayList<>(); - // Comments and strings first (highest priority) + // Comments and strings first (highest priority) - same for both languages addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT); addPatternMarks(marks, STRING_PATTERN, TokenType.STRING); + + // Keywords - same for both languages (KEYWORD_PATTERN includes JS keywords) + addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); + + // Numbers - same for both languages + addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL); + + // Language-specific marking + if (isJavaScript()) { + buildJSMarks(marks); + } else { + buildJavaMarks(marks); + } + return marks; + } + + /** + * Build marks specific to JavaScript/ECMAScript. + */ + private void buildJSMarks(List marks) { + // Mark function declarations + markJSFunctionDeclarations(marks); + + // Mark variable declarations + markJSVariableDeclarations(marks); + + // Mark member accesses with type validation + markJSMemberAccesses(marks); + + // Mark method calls + markJSMethodCalls(marks); + + // Mark standalone identifiers (parameters and variables in function bodies) + markJSIdentifiers(marks); + } + + /** + * Mark JavaScript function declarations. + */ + private void markJSFunctionDeclarations(List marks) { + Pattern funcPattern = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)"); + Matcher m = funcPattern.matcher(text); + + while (m.find()) { + if (isExcluded(m.start())) continue; + + String funcName = m.group(1); + String params = m.group(2); + int nameStart = m.start(1); + int nameEnd = m.end(1); + + // Check if this is a known hook + if (typeResolver.isJSHook(funcName)) { + List sigs = typeResolver.getJSHookSignatures(funcName); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + + // Create unified MethodInfo for hover info + TypeInfo paramTypeInfo = typeResolver.resolveJSType(sig.paramType); + List methodParams = new ArrayList<>(); + methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo)); + + MethodInfo hookMethod = MethodInfo.declaration( + funcName, null, TypeInfo.fromPrimitive("void"), methodParams, + nameStart, nameStart, nameStart, -1, -1, 0, sig.doc + ); + + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, hookMethod)); + + // Mark parameters with inferred types + if (params != null && !params.isEmpty()) { + String[] paramNames = params.split(","); + if (paramNames.length > 0) { + String paramName = paramNames[0].trim(); + int paramStart = m.start(2) + params.indexOf(paramName); + int paramEnd = paramStart + paramName.length(); + + FieldInfo paramFieldInfo = FieldInfo.parameter( + paramName, paramTypeInfo, paramStart, hookMethod + ); + marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER, paramFieldInfo)); + } + } + continue; + } + } + + // Non-hook function + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName)); + + // Mark parameters without type info + if (params != null && !params.isEmpty()) { + int paramOffset = m.start(2); + for (String p : params.split(",")) { + String pn = p.trim(); + if (!pn.isEmpty()) { + int paramStart = paramOffset + params.indexOf(pn); + int paramEnd = paramStart + pn.length(); + marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER)); + } + } + } + } + } + + /** + * Mark JavaScript variable declarations. + */ + private void markJSVariableDeclarations(List marks) { + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)"); + Matcher m = varPattern.matcher(text); + + while (m.find()) { + if (isExcluded(m.start())) continue; + + String varName = m.group(1); + int varStart = m.start(1); + int varEnd = m.end(1); + + String inferredType = jsVariableTypes.getOrDefault(varName, "any"); + TypeInfo typeInfo = typeResolver.resolveJSType(inferredType); + FieldInfo varFieldInfo = FieldInfo.localField(varName, typeInfo, varStart, null); + + marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, varFieldInfo)); + } + } + + /** + * Mark JavaScript member accesses (x.y.z) with type validation. + */ + private void markJSMemberAccesses(List marks) { + Pattern memberPattern = Pattern.compile("(\\w+)(?:\\.(\\w+))+"); + Matcher m = memberPattern.matcher(text); + + while (m.find()) { + if (isExcluded(m.start())) continue; + + String fullAccess = m.group(0); + String[] parts = fullAccess.split("\\."); + + if (parts.length < 2) continue; + + // Get receiver type + String receiverName = parts[0]; + String currentType = jsVariableTypes.get(receiverName); + + int pos = m.start(); + + // Mark receiver with unified FieldInfo + if (currentType != null) { + TypeInfo unifiedType = typeResolver.resolveJSType(currentType); + FieldInfo receiverField = FieldInfo.localField(receiverName, unifiedType, pos, null); + marks.add(new ScriptLine.Mark(pos, pos + receiverName.length(), TokenType.LOCAL_FIELD, receiverField)); + } + + pos += receiverName.length() + 1; // +1 for the dot + + // Walk the chain and mark each member + JSTypeRegistry registry = typeResolver.getJSTypeRegistry(); + for (int i = 1; i < parts.length; i++) { + String member = parts[i]; + int memberStart = pos; + int memberEnd = pos + member.length(); + + if (currentType != null) { + JSTypeInfo jsTypeInfo = registry.getType(currentType); + if (jsTypeInfo != null) { + TypeInfo unifiedContainingType = TypeInfo.fromJSTypeInfo(jsTypeInfo); + + // Check method first + JSMethodInfo jsMethod = jsTypeInfo.getMethod(member); + if (jsMethod != null) { + MethodInfo unifiedMethod = MethodInfo.fromJSMethod(jsMethod, unifiedContainingType); + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.METHOD_CALL, unifiedMethod)); + currentType = jsMethod.getReturnType(); + } else { + // Check field + JSFieldInfo jsField = jsTypeInfo.getField(member); + if (jsField != null) { + FieldInfo unifiedField = FieldInfo.fromJSField(jsField, unifiedContainingType); + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.GLOBAL_FIELD, unifiedField)); + currentType = jsField.getType(); + } else { + // Unknown member - mark as undefined + marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.UNDEFINED_VAR, + "Unknown member '" + member + "' on type " + jsTypeInfo.getFullName())); + currentType = null; + } + } + } else { + currentType = null; + } + } + + pos = memberEnd + 1; // +1 for the next dot + } + } + } + + /** + * Mark JavaScript method calls. + */ + private void markJSMethodCalls(List marks) { + Pattern callPattern = Pattern.compile("(\\w+)\\s*\\("); + Matcher m = callPattern.matcher(text); + + while (m.find()) { + if (isExcluded(m.start())) continue; + + String callExpr = m.group(1); + int nameStart = m.start(1); + int nameEnd = m.end(1); + + // Skip if it's preceded by a dot (handled by markJSMemberAccesses) + if (nameStart > 0 && text.charAt(nameStart - 1) == '.') continue; + + // Skip keywords that look like function calls + if (callExpr.equals("if") || callExpr.equals("while") || callExpr.equals("for") || + callExpr.equals("switch") || callExpr.equals("catch") || callExpr.equals("function")) { + continue; + } + + if (typeResolver.isJSHook(callExpr)) { + List sigs = typeResolver.getJSHookSignatures(callExpr); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + TypeInfo paramTypeInfo = typeResolver.resolveJSType(sig.paramType); + List methodParams = new ArrayList<>(); + methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo)); + + MethodInfo hookMethod = MethodInfo.declaration( + callExpr, null, TypeInfo.fromPrimitive("void"), methodParams, + nameStart, nameStart, nameStart, -1, -1, 0, sig.doc + ); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, hookMethod)); + } + } + } + } + + /** + * Mark standalone JavaScript identifiers (parameters and variables in function bodies). + */ + private void markJSIdentifiers(List marks) { + // Build set of positions already marked + Set markedPositions = new HashSet<>(); + for (ScriptLine.Mark mark : marks) { + for (int i = mark.start; i < mark.end; i++) { + markedPositions.add(i); + } + } + + // Find all identifiers + Pattern identPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); + Matcher matcher = identPattern.matcher(text); + + while (matcher.find()) { + if (isExcluded(matcher.start())) continue; + + int start = matcher.start(); + int end = matcher.end(); + + // Skip if already marked + boolean alreadyMarked = false; + for (int i = start; i < end; i++) { + if (markedPositions.contains(i)) { + alreadyMarked = true; + break; + } + } + if (alreadyMarked) continue; + + String identifier = matcher.group(); + + // Check if it's a parameter + boolean isParameter = false; + for (Map params : jsFunctionParams.values()) { + if (params.containsKey(identifier)) { + isParameter = true; + break; + } + } + + if (isParameter) { + marks.add(new ScriptLine.Mark(start, end, TokenType.PARAMETER)); + for (int i = start; i < end; i++) { + markedPositions.add(i); + } + } else if (jsVariableTypes.containsKey(identifier)) { + marks.add(new ScriptLine.Mark(start, end, TokenType.LOCAL_FIELD)); + for (int i = start; i < end; i++) { + markedPositions.add(i); + } + } + } + } + + /** + * Build marks specific to Java/Groovy. + */ + private void buildJavaMarks(List marks) { // Import statements markImports(marks); @@ -1527,8 +2102,7 @@ private List buildMarks() { // Enum constants (must be after class declarations so enums are known) markEnumConstants(marks); - // Keywords and modifiers - addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); + // Modifiers addPatternMarks(marks, MODIFIER_PATTERN, TokenType.MODIFIER); // Type declarations and usages @@ -1537,9 +2111,6 @@ private List buildMarks() { // Methods markMethodDeclarations(marks); - // Numbers and othe - addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL); - // Method calls (parse before variables so we can attach context) markMethodCalls(marks); @@ -1560,8 +2131,6 @@ private List buildMarks() { // Final pass: Mark any remaining unmarked identifiers as undefined markUndefinedIdentifiers(marks); - - return marks; } /** @@ -4745,6 +5314,46 @@ FieldAccessInfo createFieldAccessInfo(String name, int start, int end, } FieldInfo resolveVariable(String name, int position) { + // For JavaScript, use the JS variable tracking + if (isJavaScript()) { + return resolveJSVariable(name, position); + } + + return resolveJavaVariable(name, position); + } + + /** + * Resolve a JavaScript variable by name. + */ + private FieldInfo resolveJSVariable(String name, int position) { + // Check if it's a parameter + for (Map.Entry> entry : jsFunctionParams.entrySet()) { + if (entry.getValue().containsKey(name)) { + String typeName = entry.getValue().get(name); + TypeInfo typeInfo = typeResolver.resolveJSType(typeName); + return FieldInfo.parameter(name, typeInfo, position, null); + } + } + + // Check if it's a tracked variable + if (jsVariableTypes.containsKey(name)) { + String typeName = jsVariableTypes.get(name); + TypeInfo typeInfo = typeResolver.resolveJSType(typeName); + return FieldInfo.localField(name, typeInfo, position, null); + } + + // Check global fields (variables declared with var/let/const) + if (globalFields.containsKey(name)) { + return globalFields.get(name); + } + + return null; + } + + /** + * Resolve a Java variable by name. + */ + private FieldInfo resolveJavaVariable(String name, int position) { // Find containing method MethodInfo containingMethod = findMethodAtPosition(position); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java index 0f117c4d4..2a730efda 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java @@ -14,7 +14,18 @@ /** * Analyzes JavaScript scripts to produce syntax highlighting marks and type information. * Uses the JSTypeRegistry to resolve types and validate member access. + * + * @deprecated This class is deprecated. All JavaScript analysis is now handled by the + * unified pipeline in {@link ScriptDocument}. The functionality has been merged into: + *
    + *
  • {@link ScriptDocument#parseJSFunctions()} - for function parsing
  • + *
  • {@link ScriptDocument#parseJSVariables()} - for variable parsing
  • + *
  • {@link ScriptDocument#buildJSMarks(List)} - for mark building
  • + *
+ * Use {@link ScriptDocument#getJSVariableTypes()} and {@link ScriptDocument#getJSFunctionParams()} + * to access inferred type information. */ +@Deprecated public class JSScriptAnalyzer { private final ScriptDocument document; @@ -88,6 +99,9 @@ public List analyze() { // Mark method calls markMethodCalls(marks); + // Mark standalone identifiers (parameters and variables in function bodies) + markIdentifiers(marks); + return marks; } @@ -472,4 +486,67 @@ private TypeInfo resolveJSType(String jsTypeName) { // Fallback: unresolved type return TypeInfo.unresolved(jsTypeName, jsTypeName); } + + /** + * Mark all identifiers in the script - variables and parameters. + * This ensures consistent coloring for parameters throughout function bodies. + */ + private void markIdentifiers(List marks) { + // Build set of positions already marked + Set markedPositions = new HashSet<>(); + for (ScriptLine.Mark mark : marks) { + for (int i = mark.start; i < mark.end; i++) { + markedPositions.add(i); + } + } + + // Find all identifiers + Matcher matcher = IDENTIFIER_PATTERN.matcher(text); + while (matcher.find()) { + if (isExcluded(matcher.start())) { + continue; + } + + int start = matcher.start(); + int end = matcher.end(); + + // Skip if already marked + boolean alreadyMarked = false; + for (int i = start; i < end; i++) { + if (markedPositions.contains(i)) { + alreadyMarked = true; + break; + } + } + if (alreadyMarked) { + continue; + } + + String identifier = matcher.group(); + + // Check if it's a parameter + boolean isParameter = false; + for (Map params : functionParams.values()) { + if (params.containsKey(identifier)) { + isParameter = true; + break; + } + } + + if (isParameter) { + marks.add(new ScriptLine.Mark(start, end, TokenType.PARAMETER)); + // Mark this position as used + for (int i = start; i < end; i++) { + markedPositions.add(i); + } + } else if (variableTypes.containsKey(identifier)) { + // It's a local variable + marks.add(new ScriptLine.Mark(start, end, TokenType.LOCAL_FIELD)); + // Mark this position as used + for (int i = start; i < end; i++) { + markedPositions.add(i); + } + } + } + } } 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 741543fa2..a724b5d2e 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 @@ -324,15 +324,25 @@ public String getHookParameterType(String functionName) { * Resolve inheritance relationships between types. */ private void resolveInheritance() { + int totalInheritance = 0; + int resolvedInheritance = 0; + for (JSTypeInfo type : types.values()) { String extendsType = type.getExtendsType(); if (extendsType != null) { + totalInheritance++; JSTypeInfo parent = getType(extendsType); if (parent != null) { type.setResolvedParent(parent); + resolvedInheritance++; + System.out.println("[JSTypeRegistry] Resolved " + type.getSimpleName() + " extends " + parent.getSimpleName()); + } else { + System.err.println("[JSTypeRegistry] FAILED to resolve parent type '" + extendsType + "' for " + type.getFullName()); } } } + + System.out.println("[JSTypeRegistry] Inheritance resolution: " + resolvedInheritance + "/" + totalInheritance + " successful"); } /** 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 1be0617d7..297256f97 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 @@ -1,15 +1,22 @@ package noppes.npcs.client.gui.util.script.interpreter.type; import noppes.npcs.client.gui.util.script.PackageFinder; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import java.util.*; /** - * Resolves class/interface/enum types from import paths and simple names. - * Maintains caches for performance and handles inner class resolution. + * Unified type resolver for both Java and JavaScript/TypeScript types. + * Serves as the single front-end for all type resolution needs. * - * This is a clean reimplementation of ClassPathFinder with better OOP structure. + * For Java: Uses reflection and import resolution + * For JavaScript: Delegates to JSTypeRegistry for .d.ts defined types + * + * This is the ONLY class that should be used for type resolution. */ public class TypeResolver { @@ -18,6 +25,9 @@ public class TypeResolver { // Cache: validated package paths private final Set validPackages = new HashSet<>(); + + // JavaScript type registry (lazily initialized) + private JSTypeRegistry jsTypeRegistry; // Auto-imported java.lang classes public static final Set JAVA_LANG_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( @@ -30,6 +40,19 @@ public class TypeResolver { "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" ))); + + // JavaScript primitive type mappings + private static final Map JS_PRIMITIVE_TO_JAVA = new HashMap<>(); + static { + JS_PRIMITIVE_TO_JAVA.put("string", "String"); + JS_PRIMITIVE_TO_JAVA.put("number", "double"); + JS_PRIMITIVE_TO_JAVA.put("boolean", "boolean"); + JS_PRIMITIVE_TO_JAVA.put("any", "Object"); + JS_PRIMITIVE_TO_JAVA.put("void", "void"); + JS_PRIMITIVE_TO_JAVA.put("null", "null"); + JS_PRIMITIVE_TO_JAVA.put("undefined", "void"); + JS_PRIMITIVE_TO_JAVA.put("object", "Object"); + } // Singleton instance for global caching (optional - can also use per-document instances) private static TypeResolver instance; @@ -48,6 +71,42 @@ public TypeResolver() { validPackages.add("java.util"); validPackages.add("java.io"); } + + // ==================== JS TYPE REGISTRY ACCESS ==================== + + /** + * Get the JS type registry, initializing it if needed. + */ + public JSTypeRegistry getJSTypeRegistry() { + if (jsTypeRegistry == null) { + jsTypeRegistry = JSTypeRegistry.getInstance(); + if (!jsTypeRegistry.isInitialized()) { + jsTypeRegistry.initializeFromResources(); + } + } + return jsTypeRegistry; + } + + /** + * Check if a function name is a known JS hook. + */ + public boolean isJSHook(String functionName) { + return getJSTypeRegistry().isHook(functionName); + } + + /** + * Get the parameter type for a JS hook function. + */ + public String getJSHookParameterType(String functionName) { + return getJSTypeRegistry().getHookParameterType(functionName); + } + + /** + * Get hook signatures for a JS function. + */ + public List getJSHookSignatures(String functionName) { + return getJSTypeRegistry().getHookSignatures(functionName); + } // ==================== CACHE MANAGEMENT ==================== @@ -63,6 +122,57 @@ public void clearCache() { validPackages.add("java.util"); validPackages.add("java.io"); } + + // ==================== UNIFIED TYPE RESOLUTION ==================== + + /** + * Resolve a type name for JavaScript context. + * Handles JS primitives, .d.ts types, and falls back to Java types. + * + * @param jsTypeName The JS type name (e.g., "IPlayer", "string", "number") + * @return TypeInfo for the resolved type, or unresolved TypeInfo if not found + */ + public TypeInfo resolveJSType(String jsTypeName) { + if (jsTypeName == null || jsTypeName.isEmpty()) { + return TypeInfo.fromPrimitive("void"); + } + + // Strip array brackets for base type resolution + boolean isArray = jsTypeName.endsWith("[]"); + String baseName = isArray ? jsTypeName.substring(0, jsTypeName.length() - 2) : jsTypeName; + + // Check JS primitives first + if (JS_PRIMITIVE_TO_JAVA.containsKey(baseName)) { + String javaType = JS_PRIMITIVE_TO_JAVA.get(baseName); + TypeInfo baseType = TypeResolver.isPrimitiveType(javaType) + ? TypeInfo.fromPrimitive(javaType) + : resolveFullName("java.lang." + javaType); + return isArray ? TypeInfo.arrayOf(baseType) : baseType; + } + + // Check JS type registry + JSTypeInfo jsTypeInfo = getJSTypeRegistry().getType(baseName); + if (jsTypeInfo != null) { + TypeInfo baseType = TypeInfo.fromJSTypeInfo(jsTypeInfo); + return isArray ? TypeInfo.arrayOf(baseType) : baseType; + } + + // Fall back to Java type resolution + TypeInfo javaType = resolveFullName(baseName); + if (javaType != null && javaType.isResolved()) { + return isArray ? TypeInfo.arrayOf(javaType) : javaType; + } + + // Unresolved + return TypeInfo.unresolved(jsTypeName, jsTypeName); + } + + /** + * Check if a JS type name is a primitive. + */ + public boolean isJSPrimitive(String typeName) { + return JS_PRIMITIVE_TO_JAVA.containsKey(typeName); + } // ==================== TYPE RESOLUTION ==================== From 1221badc699f51ac4f5073d73c0679e43e1d7cc4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 22:13:47 +0200 Subject: [PATCH 197/337] Unified Java/JS handling into ScriptDocument. much better now. --- .../script/interpreter/ScriptDocument.java | 1157 ++++++----------- .../js_parser/JSScriptAnalyzer.java | 14 +- 2 files changed, 378 insertions(+), 793 deletions(-) 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 e004fb2e4..51f83e190 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 @@ -238,20 +238,17 @@ public void init(int width, int height) { } // ==================== TOKENIZATION ==================== - - // JS-specific: Function parameter types (funcName -> paramName -> typeName) - private final Map> jsFunctionParams = new HashMap<>(); - - // JS-specific: Inferred variable types (varName -> typeName) - private final Map jsVariableTypes = new HashMap<>(); /** * Main tokenization entry point. * Performs complete analysis and builds tokens for all lines. - * Uses a UNIFIED pipeline for both Java and JavaScript. + * Uses the SAME unified pipeline for both Java and JavaScript. + * + * All data structures (methods, globalFields, methodLocals, methodCalls, + * fieldAccesses, etc.) are shared between languages. */ public void formatCodeText() { - // Clear previous state (common for both languages) + // Clear previous state (same for both languages) imports.clear(); methods.clear(); globalFields.clear(); @@ -262,8 +259,6 @@ public void formatCodeText() { methodCalls.clear(); externalFieldAssignments.clear(); declarationErrors.clear(); - jsFunctionParams.clear(); - jsVariableTypes.clear(); // Unified pipeline for both languages List marks = formatUnified(); @@ -280,35 +275,21 @@ public void formatCodeText() { computeIndentGuides(marks); } - // Store the last JS analyzer for autocomplete (deprecated - use getJSVariableTypes instead) + // Store the last JS analyzer for autocomplete (deprecated - use methods/globalFields/methodLocals instead) @Deprecated private JSScriptAnalyzer currentJSAnalyzer; /** - * @deprecated Use the unified pipeline. Variable types are now in jsVariableTypes. + * @deprecated Use the unified pipeline. Access methods/globalFields/methodLocals directly. */ @Deprecated public JSScriptAnalyzer getJSAnalyzer() { return currentJSAnalyzer; } - /** - * Get inferred JS variable types (for autocomplete). - */ - public Map getJSVariableTypes() { - return new HashMap<>(jsVariableTypes); - } - - /** - * Get JS function parameter types. - */ - public Map> getJSFunctionParams() { - return new HashMap<>(jsFunctionParams); - } - /** * Unified format method - single pipeline for both Java and JavaScript. - * The methods called here are language-aware and handle both cases. + * ALL methods called here handle BOTH languages using the SAME data structures. */ private List formatUnified() { // Phase 1: Find excluded regions (strings/comments) - same for both @@ -451,6 +432,10 @@ private void parseImports() { // ==================== PHASE 3: STRUCTURE ==================== + /** + * Parse the script structure - methods, fields, variables. + * Uses the SAME logic and data structures for both Java and JavaScript. + */ private void parseStructure() { // Clear import references before re-parsing for (ImportData imp : imports) { @@ -462,21 +447,17 @@ private void parseStructure() { parseScriptTypes(); } - // Parse methods/functions - language aware + // Parse methods/functions - UNIFIED for both languages parseMethodDeclarations(); - // Parse local variables inside methods/functions - language aware + // Parse local variables inside methods/functions - UNIFIED for both languages parseLocalVariables(); - // Parse global fields (outside methods) - Java only - if (!isJavaScript()) { - parseGlobalFields(); - } + // Parse global fields (outside methods) - UNIFIED for both languages + parseGlobalFields(); - // Parse and validate assignments (reassignments) - Java only - if (!isJavaScript()) { - parseAssignments(); - } + // Parse and validate assignments (reassignments) - UNIFIED for both languages + parseAssignments(); // Detect method overrides and interface implementations for script types - Java only if (!isJavaScript()) { @@ -673,171 +654,148 @@ private boolean isInsideNestedMethod(int position, int classBodyStart, int class return false; } - private void parseMethodDeclarations() { - if (isJavaScript()) { - parseJSFunctions(); - } else { - parseJavaMethods(); - } - } - /** - * Parse JavaScript function declarations and infer parameter types from hooks. + * Parse method/function declarations - UNIFIED for both Java and JavaScript. + * Stores results in the shared 'methods' list. + * + * For Java: Parses "ReturnType methodName(params) { ... }" + * For JavaScript: Parses "function funcName(params) { ... }" with hook type inference */ - private void parseJSFunctions() { - // Pattern: function funcName(params) { ... } - Pattern funcPattern = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*\\{"); - Matcher m = funcPattern.matcher(text); - - while (m.find()) { - if (isExcluded(m.start())) continue; - - String funcName = m.group(1); - String paramList = m.group(2); - - int nameStart = m.start(1); - int nameEnd = m.end(1); - int bodyStart = text.indexOf('{', m.end() - 1); - int bodyEnd = findMatchingBrace(bodyStart); - if (bodyEnd < 0) bodyEnd = text.length(); - - // Check if this is a known hook function - List params = new ArrayList<>(); - TypeInfo returnType = TypeInfo.fromPrimitive("void"); - String documentation = null; + private void parseMethodDeclarations() { + if (isJavaScript()) { + // JavaScript: function funcName(params) { ... } + Pattern funcPattern = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*\\{"); + Matcher m = funcPattern.matcher(text); - if (typeResolver.isJSHook(funcName)) { - List sigs = typeResolver.getJSHookSignatures(funcName); - if (!sigs.isEmpty()) { - JSTypeRegistry.HookSignature sig = sigs.get(0); - documentation = sig.doc; - - // Infer parameter types from hook signature - if (paramList != null && !paramList.trim().isEmpty()) { - String[] paramNames = paramList.split(","); - if (paramNames.length > 0) { - String paramName = paramNames[0].trim(); - String paramTypeName = sig.paramType; - TypeInfo paramType = typeResolver.resolveJSType(paramTypeName); - - // Store in tracking maps for later use - Map funcParams = new HashMap<>(); - funcParams.put(paramName, paramTypeName); - jsFunctionParams.put(funcName, funcParams); - jsVariableTypes.put(paramName, paramTypeName); - - // Create parameter with proper type - int paramStart = m.start(2) + paramList.indexOf(paramName); - params.add(FieldInfo.parameter(paramName, paramType, paramStart, null)); + while (m.find()) { + if (isExcluded(m.start())) continue; + + String funcName = m.group(1); + String paramList = m.group(2); + + int nameStart = m.start(1); + int bodyStart = text.indexOf('{', m.end() - 1); + int bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) bodyEnd = text.length(); + + // For JS hooks, infer parameter types from registry + List params = new ArrayList<>(); + TypeInfo returnType = TypeInfo.fromPrimitive("void"); + String documentation = null; + + if (typeResolver.isJSHook(funcName)) { + List sigs = typeResolver.getJSHookSignatures(funcName); + if (!sigs.isEmpty()) { + JSTypeRegistry.HookSignature sig = sigs.get(0); + documentation = sig.doc; + + // Infer parameter types from hook signature + if (paramList != null && !paramList.trim().isEmpty()) { + String[] paramNames = paramList.split(","); + if (paramNames.length > 0) { + String paramName = paramNames[0].trim(); + TypeInfo paramType = typeResolver.resolveJSType(sig.paramType); + int paramStart = m.start(2) + paramList.indexOf(paramName); + params.add(FieldInfo.parameter(paramName, paramType, paramStart, null)); + } } } - } - } else { - // Non-hook function - parameters have no type - if (paramList != null && !paramList.trim().isEmpty()) { - int paramOffset = m.start(2); - for (String p : paramList.split(",")) { - String pn = p.trim(); - if (!pn.isEmpty()) { - int paramStart = paramOffset + paramList.indexOf(pn); - params.add(FieldInfo.parameter(pn, TypeInfo.fromPrimitive("any"), paramStart, null)); - - // Track as untyped parameter - Map funcParams = jsFunctionParams.computeIfAbsent(funcName, k -> new HashMap<>()); - funcParams.put(pn, "any"); + } else { + // Non-hook function - params with 'any' type + if (paramList != null && !paramList.trim().isEmpty()) { + int paramOffset = m.start(2); + for (String p : paramList.split(",")) { + String pn = p.trim(); + if (!pn.isEmpty()) { + int paramStart = paramOffset + paramList.indexOf(pn); + params.add(FieldInfo.parameter(pn, TypeInfo.fromClass(Object.class), paramStart, null)); + } } } } + + // Create MethodInfo and add to shared methods list + MethodInfo methodInfo = MethodInfo.declaration( + funcName, null, returnType, params, + m.start(), nameStart, nameStart, + bodyStart, bodyEnd, 0, documentation + ); + methods.add(methodInfo); } - - // Create MethodInfo for the function - MethodInfo methodInfo = MethodInfo.declaration( - funcName, null, returnType, params, - m.start(), nameStart, nameStart, - bodyStart, bodyEnd, 0, documentation - ); - - methods.add(methodInfo); - } - } - - /** - * Parse Java method declarations. - */ - private void parseJavaMethods() { - Pattern methodWithBody = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*(\\{|;)"); + } else { + // Java: ReturnType methodName(params) { ... } or { ; } + Pattern methodWithBody = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*(\\{|;)"); - Matcher m = methodWithBody.matcher(text); - while (m.find()) { - if (isExcluded(m.start())) - continue; + Matcher m = methodWithBody.matcher(text); + while (m.find()) { + if (isExcluded(m.start())) + continue; - String returnType = m.group(1); + String returnType = m.group(1); - if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum") ||returnType.equals("new")) { - continue; - } - - String methodName = m.group(2); - String paramList = m.group(3); - String delimiter = m.group(4); - boolean hasBody = delimiter.equals("{"); - - int bodyStart = !hasBody ? m.end() : text.indexOf('{', m.end() - 1); - int bodyEnd = !hasBody ? m.end() : findMatchingBrace(bodyStart); - if (bodyEnd < 0) - bodyEnd = text.length(); + if (returnType.equals("class") || returnType.equals("interface") || returnType.equals("enum") || returnType.equals("new")) { + continue; + } + + String methodName = m.group(2); + String paramList = m.group(3); + String delimiter = m.group(4); + boolean hasBody = delimiter.equals("{"); + int bodyStart = !hasBody ? m.end() : text.indexOf('{', m.end() - 1); + int bodyEnd = !hasBody ? m.end() : findMatchingBrace(bodyStart); + if (bodyEnd < 0) + bodyEnd = text.length(); - // Extract documentation before this method - String documentation = extractDocumentationBefore(m.start()); + // Extract documentation before this method + String documentation = extractDocumentationBefore(m.start()); - // Extract modifiers by scanning backwards from match start - int modifiers = extractModifiersBackwards(m.start() - 1, text); - - // Calculate offset positions - int typeOffset = m.start(1); // Start of return type - int nameOffset = m.start(2); // Start of method name - int fullDeclOffset = findFullDeclarationStart(m.start(1), text); // Start including modifiers - - ScriptTypeInfo scriptType = null; - for (ScriptTypeInfo type : scriptTypes.values()) - if (type.containsPosition(bodyStart)) { - scriptType = type; - break; - } - - // Parse parameters with their actual positions - List params = parseParametersWithPositions(paramList, m.start(3)); + // Extract modifiers by scanning backwards from match start + int modifiers = extractModifiersBackwards(m.start() - 1, text); + + // Calculate offset positions + int typeOffset = m.start(1); + int nameOffset = m.start(2); + int fullDeclOffset = findFullDeclarationStart(m.start(1), text); + + ScriptTypeInfo scriptType = null; + for (ScriptTypeInfo type : scriptTypes.values()) + if (type.containsPosition(bodyStart)) { + scriptType = type; + break; + } + + // Parse parameters with their actual positions + List params = parseParametersWithPositions(paramList, m.start(3)); - MethodInfo methodInfo = MethodInfo.declaration( - methodName, - scriptType, - resolveType(returnType), - params, - fullDeclOffset, - typeOffset, - nameOffset, - bodyStart, - bodyEnd, - modifiers, - documentation - ); + MethodInfo methodInfo = MethodInfo.declaration( + methodName, + scriptType, + resolveType(returnType), + params, + fullDeclOffset, + typeOffset, + nameOffset, + bodyStart, + bodyEnd, + modifiers, + documentation + ); + if (scriptType != null) { + scriptType.addMethod(methodInfo); + } else + methods.add(methodInfo); - if (scriptType != null) { - scriptType.addMethod(methodInfo); - } else - methods.add(methodInfo); + // Validate the method (return statements, parameters) with type resolution + String methodBodyText = bodyEnd > bodyStart + 1 ? text.substring(bodyStart + 1, bodyEnd) : ""; + methodInfo.validate(methodBodyText, hasBody, (expr, pos) -> resolveExpressionType(expr, pos)); + } - // Validate the method (return statements, parameters) with type resolution - String methodBodyText = bodyEnd > bodyStart + 1 ? text.substring(bodyStart + 1, bodyEnd) : ""; - methodInfo.validate(methodBodyText, hasBody, (expr, pos) -> resolveExpressionType(expr, pos)); + // Check for duplicate method declarations + checkDuplicateMethods(); } - - // Check for duplicate method declarations - checkDuplicateMethods(); } /** @@ -995,145 +953,14 @@ private List parseParametersWithPositions(String paramList, int param return params; } - private void parseLocalVariables() { - if (isJavaScript()) { - parseJSVariables(); - } else { - parseJavaLocalVariables(); - } - } - /** - * Parse JavaScript variable declarations (var/let/const). - */ - private void parseJSVariables() { - // Pattern: var/let/const varName = expression; or var/let/const varName; - Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;]+))?"); - Matcher m = varPattern.matcher(text); - - while (m.find()) { - if (isExcluded(m.start())) continue; - - String varName = m.group(1); - String initializer = m.group(2); - - int varStart = m.start(1); - int varEnd = m.end(1); - - // Infer type from initializer - String inferredType = "any"; - if (initializer != null) { - inferredType = inferJSTypeFromExpression(initializer.trim()); - } - - // Store in tracking maps - jsVariableTypes.put(varName, inferredType); - - TypeInfo typeInfo = typeResolver.resolveJSType(inferredType); - - // Store as a global field (JS doesn't have method-scoped locals in the same way) - FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, varStart, null); - globalFields.put(varName, fieldInfo); - } - } - - /** - * Infer type from a JavaScript expression. - */ - private String inferJSTypeFromExpression(String expr) { - if (expr == null || expr.isEmpty()) return "any"; - - // String literal - if (expr.startsWith("\"") || expr.startsWith("'")) { - return "string"; - } - - // Number literal - if (expr.matches("-?\\d+\\.?\\d*")) { - return "number"; - } - - // Boolean literal - if (expr.equals("true") || expr.equals("false")) { - return "boolean"; - } - - // Null/undefined - if (expr.equals("null")) return "null"; - if (expr.equals("undefined")) return "undefined"; - - // Array literal - if (expr.startsWith("[")) { - return "any[]"; - } - - // Object literal - if (expr.startsWith("{")) { - return "object"; - } - - // Method call chain: something.method() - infer from method return type - if (expr.contains(".") && expr.contains("(")) { - return inferJSTypeFromMethodCall(expr); - } - - // Variable reference - if (jsVariableTypes.containsKey(expr)) { - return jsVariableTypes.get(expr); - } - - return "any"; - } - - /** - * Infer type from a JavaScript method call chain. - */ - private String inferJSTypeFromMethodCall(String expr) { - // Remove trailing parentheses and args for analysis - int parenIndex = expr.indexOf('('); - if (parenIndex > 0) { - expr = expr.substring(0, parenIndex); - } - - String[] parts = expr.split("\\."); - if (parts.length < 2) return "any"; - - // Start with the receiver type - String currentType = jsVariableTypes.get(parts[0]); - if (currentType == null) return "any"; - - // Walk the chain - JSTypeRegistry registry = typeResolver.getJSTypeRegistry(); - for (int i = 1; i < parts.length; i++) { - JSTypeInfo typeInfo = registry.getType(currentType); - if (typeInfo == null) return "any"; - - String member = parts[i]; - - // Check if it's a method - JSMethodInfo method = typeInfo.getMethod(member); - if (method != null) { - currentType = method.getReturnType(); - continue; - } - - // Check if it's a field - JSFieldInfo field = typeInfo.getField(member); - if (field != null) { - currentType = field.getType(); - continue; - } - - return "any"; // Unknown member - } - - return currentType; - } - - /** - * Parse Java local variables inside methods. + * Parse local variables inside methods/functions - UNIFIED for both Java and JavaScript. + * Stores results in the shared 'methodLocals' map (methodOffset -> varName -> FieldInfo). + * + * For Java: Parses "Type varName = expr;" or "Type varName;" + * For JavaScript: Parses "var/let/const varName = expr;" with type inference */ - private void parseJavaLocalVariables() { + private void parseLocalVariables() { for (MethodInfo method : getAllMethods()) { Map locals = new HashMap<>(); methodLocals.put(method.getDeclarationOffset(), locals); @@ -1144,74 +971,102 @@ private void parseJavaLocalVariables() { String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length())); - // Pattern for local variable declarations: Type varName = or Type varName; - // Allows capital var names like "Minecraft Capital = new Minecraft();" - // Use [ \t] instead of \s to prevent matching across newlines - - Matcher m = FIELD_DECL_PATTERN.matcher(bodyText); - while (m.find()) { - String typeName = m.group(1); - String varName = m.group(2); - String delimiter = m.group(3); + if (isJavaScript()) { + // JavaScript: var/let/const varName = expr; + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;\\n]+))?"); + Matcher m = varPattern.matcher(bodyText); - // Skip if the variable name itself is excluded - int absPos = bodyStart + m.start(2); - if (isExcluded(absPos)) continue; - - // Skip if it looks like a method call or control flow - if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || - typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || - typeName.equals("new") || typeName.equals("throw")) { - continue; + while (m.find()) { + int absPos = bodyStart + m.start(1); + if (isExcluded(absPos)) continue; + + String varName = m.group(1); + String initializer = m.group(2); + + // Infer type from initializer using resolveExpressionType + TypeInfo typeInfo = null; + if (initializer != null && !initializer.trim().isEmpty()) { + typeInfo = resolveExpressionType(initializer.trim(), bodyStart + m.start(2)); + } + if (typeInfo == null) { + typeInfo = TypeInfo.fromClass(Object.class); // Default to Object for unresolved + } + + int initStart = -1, initEnd = -1; + if (initializer != null) { + initStart = bodyStart + m.start(2); + initEnd = bodyStart + m.end(2); + } + + FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, absPos, method, initStart, initEnd, 0); + + // Check for duplicate + if (locals.containsKey(varName) || globalFields.containsKey(varName)) { + AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( + varName, absPos, absPos + varName.length(), + "Variable '" + varName + "' is already defined in the scope"); + declarationErrors.add(dupError); + } else { + locals.put(varName, fieldInfo); + } } + } else { + // Java: Type varName = expr; or Type varName; + Matcher m = FIELD_DECL_PATTERN.matcher(bodyText); + while (m.find()) { + String typeName = m.group(1); + String varName = m.group(2); + String delimiter = m.group(3); + + int absPos = bodyStart + m.start(2); + if (isExcluded(absPos)) continue; + + // Skip control flow keywords + if (typeName.equals("return") || typeName.equals("if") || typeName.equals("while") || + typeName.equals("for") || typeName.equals("switch") || typeName.equals("catch") || + typeName.equals("new") || typeName.equals("throw")) { + continue; + } - // Parse modifiers from the raw type declaration - int modifiers = parseModifiers(typeName); + int modifiers = parseModifiers(typeName); - TypeInfo typeInfo; - - // For var/let/const, infer type from the right-hand side expression - if ((typeName.equals("var") || typeName.equals("let") || typeName.equals("const")) - && delimiter.equals("=")) { - // Find the right-hand side expression - int rhsStart = bodyStart + m.end(); - typeInfo = inferTypeFromExpression(rhsStart); - } else { - typeInfo = resolveType(typeName); - } - - // Extract initialization range if there's an '=' delimiter - int initStart = -1; - int initEnd = -1; - if ("=".equals(delimiter)) { - initStart = bodyStart + m.start(3); // Absolute position of '=' - // Find the semicolon or comma that ends this declaration - int searchPos = bodyStart + m.end(3); - int depth = 0; // Track nested parens/brackets/braces - while (searchPos < text.length()) { - char c = text.charAt(searchPos); - if (c == '(' || c == '[' || c == '{') depth++; - else if (c == ')' || c == ']' || c == '}') depth--; - else if ((c == ';' || c == ',') && depth == 0) { - initEnd = searchPos; // Position of ';' or ',' (exclusive) - break; + TypeInfo typeInfo; + if ((typeName.equals("var") || typeName.equals("let") || typeName.equals("const")) + && delimiter.equals("=")) { + int rhsStart = bodyStart + m.end(); + typeInfo = inferTypeFromExpression(rhsStart); + } else { + typeInfo = resolveType(typeName); + } + + int initStart = -1, initEnd = -1; + if ("=".equals(delimiter)) { + initStart = bodyStart + m.start(3); + int searchPos = bodyStart + m.end(3); + int depth = 0; + while (searchPos < text.length()) { + char c = text.charAt(searchPos); + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') depth--; + else if ((c == ';' || c == ',') && depth == 0) { + initEnd = searchPos; + break; + } + searchPos++; } - searchPos++; } - } - - int declPos = bodyStart + m.start(2); - FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method, initStart, initEnd, modifiers); + + int declPos = bodyStart + m.start(2); + FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, declPos, method, initStart, initEnd, modifiers); - // Check for duplicate declaration (in locals or globals) - if (locals.containsKey(varName) || globalFields.containsKey(varName)) { - // Create a declaration error for this duplicate - AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( + if (locals.containsKey(varName) || globalFields.containsKey(varName)) { + AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( varName, declPos, declPos + varName.length(), "Variable '" + varName + "' is already defined in the scope"); - declarationErrors.add(dupError); - } else if (!locals.containsKey(varName)) - locals.put(varName, fieldInfo); + declarationErrors.add(dupError); + } else if (!locals.containsKey(varName)) + locals.put(varName, fieldInfo); + } } } } @@ -1402,66 +1257,115 @@ private TypeInfo inferChainType(String firstIdent, int identStart, int dotPositi return currentType; } + /** + * Parse global fields/variables (outside methods) - UNIFIED for both Java and JavaScript. + * Stores results in the shared 'globalFields' map. + * + * For Java: Parses "[modifiers] Type fieldName [= expr];" + * For JavaScript: Parses "var/let/const varName [= expr];" outside of functions + */ private void parseGlobalFields() { - Matcher m = FIELD_DECL_PATTERN.matcher(text); - while (m.find()) { - String typeNameRaw = m.group(1); - String fieldName = m.group(2); - String delimiter = m.group(3); - int position = m.start(2); + if (isJavaScript()) { + // JavaScript: var/let/const varName = expr; (outside functions) + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;\\n]+))?"); + Matcher m = varPattern.matcher(text); - // Skip if the field name itself is excluded - if (isExcluded(position)) - continue; - - // Parse modifiers from the raw type declaration - int modifiers = parseModifiers(typeNameRaw); - - // Strip modifiers (public, private, protected, static, final, etc.) from type name - String typeName = stripModifiers(typeNameRaw); - - ScriptTypeInfo containingScriptType = null; - for (ScriptTypeInfo scriptType : scriptTypes.values()) - if (scriptType.containsPosition(position)) { - containingScriptType = scriptType; - break; - } - - // Check if inside a method - if so, it's a local, not global - boolean insideMethod = false; - if (containingScriptType != null && isInsideNestedMethod(position, containingScriptType.getBodyStart(), - containingScriptType.getBodyEnd())) - insideMethod = true; - else { + while (m.find()) { + int position = m.start(1); + if (isExcluded(position)) continue; + + // Check if inside a function - if so, skip (handled by parseLocalVariables) + boolean insideMethod = false; for (MethodInfo method : getAllMethods()) { if (method.containsPosition(position)) { insideMethod = true; break; } } + if (insideMethod) continue; + + String varName = m.group(1); + String initializer = m.group(2); + + // Infer type from initializer + TypeInfo typeInfo = null; + if (initializer != null && !initializer.trim().isEmpty()) { + typeInfo = resolveExpressionType(initializer.trim(), m.start(2)); + } + if (typeInfo == null) { + typeInfo = TypeInfo.fromClass(Object.class); + } + + int initStart = -1, initEnd = -1; + if (initializer != null) { + initStart = m.start(2); + initEnd = m.end(2); + } + + FieldInfo fieldInfo = FieldInfo.globalField(varName, typeInfo, position, null, initStart, initEnd, 0); + + if (globalFields.containsKey(varName)) { + AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( + varName, position, position + varName.length(), + "Variable '" + varName + "' is already defined in the scope"); + declarationErrors.add(dupError); + } else { + globalFields.put(varName, fieldInfo); + } } + } else { + // Java: [modifiers] Type fieldName [= expr]; + Matcher m = FIELD_DECL_PATTERN.matcher(text); + while (m.find()) { + String typeNameRaw = m.group(1); + String fieldName = m.group(2); + String delimiter = m.group(3); + int position = m.start(2); + + if (isExcluded(position)) + continue; + int modifiers = parseModifiers(typeNameRaw); + String typeName = stripModifiers(typeNameRaw); - if (insideMethod) - continue; - - // Extract documentation before this field + ScriptTypeInfo containingScriptType = null; + for (ScriptTypeInfo scriptType : scriptTypes.values()) + if (scriptType.containsPosition(position)) { + containingScriptType = scriptType; + break; + } + + // Check if inside a method - if so, it's a local, not global + boolean insideMethod = false; + if (containingScriptType != null && isInsideNestedMethod(position, containingScriptType.getBodyStart(), + containingScriptType.getBodyEnd())) + insideMethod = true; + else { + for (MethodInfo method : getAllMethods()) { + if (method.containsPosition(position)) { + insideMethod = true; + break; + } + } + } + + if (insideMethod) + continue; + String documentation = extractDocumentationBefore(m.start()); - // Extract initialization range if there's an '=' delimiter int initStart = -1; int initEnd = -1; if ("=".equals(delimiter)) { - initStart = m.start(3); // Position of '=' - // Find the semicolon that ends this declaration + initStart = m.start(3); int searchPos = m.end(3); - int depth = 0; // Track nested parens/brackets/braces + int depth = 0; while (searchPos < text.length()) { char c = text.charAt(searchPos); if (c == '(' || c == '[' || c == '{') depth++; else if (c == ')' || c == ']' || c == '}') depth--; else if (c == ';' && depth == 0) { - initEnd = searchPos; // Position of ';' (exclusive) + initEnd = searchPos; break; } searchPos++; @@ -1471,19 +1375,17 @@ private void parseGlobalFields() { TypeInfo typeInfo = resolveType(typeName); FieldInfo fieldInfo = FieldInfo.globalField(fieldName, typeInfo, position, documentation, initStart, initEnd, modifiers); - - if (containingScriptType != null) { - containingScriptType.addField(fieldInfo); - // Check for duplicate declaration - } else if (globalFields.containsKey(fieldName)) { - // Create a declaration error for this duplicate + if (containingScriptType != null) { + containingScriptType.addField(fieldInfo); + } else if (globalFields.containsKey(fieldName)) { AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( - fieldName, position, position + fieldName.length(), - "Variable '" + fieldName + "' is already defined in the scope"); + fieldName, position, position + fieldName.length(), + "Variable '" + fieldName + "' is already defined in the scope"); declarationErrors.add(dupError); } else { globalFields.put(fieldName, fieldInfo); } + } } } @@ -1785,6 +1687,10 @@ private TypeInfo resolveTypeAndTrackUsage(String typeName) { // ==================== PHASE 4: BUILD MARKS ==================== + /** + * Build syntax highlighting marks - UNIFIED for both Java and JavaScript. + * Uses the SAME mark methods for both languages since they share data structures. + */ private List buildMarks() { List marks = new ArrayList<>(); @@ -1792,345 +1698,56 @@ private List buildMarks() { addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT); addPatternMarks(marks, STRING_PATTERN, TokenType.STRING); - // Keywords - same for both languages (KEYWORD_PATTERN includes JS keywords) + // Keywords - same for both languages (KEYWORD_PATTERN includes JS keywords like function, var, let, const) addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); // Numbers - same for both languages addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL); - // Language-specific marking - if (isJavaScript()) { - buildJSMarks(marks); - } else { - buildJavaMarks(marks); + // Import statements - Java only + if (!isJavaScript()) { + markImports(marks); } - return marks; - } - - /** - * Build marks specific to JavaScript/ECMAScript. - */ - private void buildJSMarks(List marks) { - // Mark function declarations - markJSFunctionDeclarations(marks); - - // Mark variable declarations - markJSVariableDeclarations(marks); - - // Mark member accesses with type validation - markJSMemberAccesses(marks); - - // Mark method calls - markJSMethodCalls(marks); - - // Mark standalone identifiers (parameters and variables in function bodies) - markJSIdentifiers(marks); - } - - /** - * Mark JavaScript function declarations. - */ - private void markJSFunctionDeclarations(List marks) { - Pattern funcPattern = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)"); - Matcher m = funcPattern.matcher(text); - - while (m.find()) { - if (isExcluded(m.start())) continue; - - String funcName = m.group(1); - String params = m.group(2); - int nameStart = m.start(1); - int nameEnd = m.end(1); - - // Check if this is a known hook - if (typeResolver.isJSHook(funcName)) { - List sigs = typeResolver.getJSHookSignatures(funcName); - if (!sigs.isEmpty()) { - JSTypeRegistry.HookSignature sig = sigs.get(0); - - // Create unified MethodInfo for hover info - TypeInfo paramTypeInfo = typeResolver.resolveJSType(sig.paramType); - List methodParams = new ArrayList<>(); - methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo)); - - MethodInfo hookMethod = MethodInfo.declaration( - funcName, null, TypeInfo.fromPrimitive("void"), methodParams, - nameStart, nameStart, nameStart, -1, -1, 0, sig.doc - ); - - marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, hookMethod)); - - // Mark parameters with inferred types - if (params != null && !params.isEmpty()) { - String[] paramNames = params.split(","); - if (paramNames.length > 0) { - String paramName = paramNames[0].trim(); - int paramStart = m.start(2) + params.indexOf(paramName); - int paramEnd = paramStart + paramName.length(); - - FieldInfo paramFieldInfo = FieldInfo.parameter( - paramName, paramTypeInfo, paramStart, hookMethod - ); - marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER, paramFieldInfo)); - } - } - continue; - } - } - - // Non-hook function - marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName)); - - // Mark parameters without type info - if (params != null && !params.isEmpty()) { - int paramOffset = m.start(2); - for (String p : params.split(",")) { - String pn = p.trim(); - if (!pn.isEmpty()) { - int paramStart = paramOffset + params.indexOf(pn); - int paramEnd = paramStart + pn.length(); - marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER)); - } - } - } - } - } - - /** - * Mark JavaScript variable declarations. - */ - private void markJSVariableDeclarations(List marks) { - Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)"); - Matcher m = varPattern.matcher(text); - - while (m.find()) { - if (isExcluded(m.start())) continue; - - String varName = m.group(1); - int varStart = m.start(1); - int varEnd = m.end(1); - - String inferredType = jsVariableTypes.getOrDefault(varName, "any"); - TypeInfo typeInfo = typeResolver.resolveJSType(inferredType); - FieldInfo varFieldInfo = FieldInfo.localField(varName, typeInfo, varStart, null); - - marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, varFieldInfo)); - } - } - - /** - * Mark JavaScript member accesses (x.y.z) with type validation. - */ - private void markJSMemberAccesses(List marks) { - Pattern memberPattern = Pattern.compile("(\\w+)(?:\\.(\\w+))+"); - Matcher m = memberPattern.matcher(text); - - while (m.find()) { - if (isExcluded(m.start())) continue; - - String fullAccess = m.group(0); - String[] parts = fullAccess.split("\\."); - - if (parts.length < 2) continue; - - // Get receiver type - String receiverName = parts[0]; - String currentType = jsVariableTypes.get(receiverName); - - int pos = m.start(); - - // Mark receiver with unified FieldInfo - if (currentType != null) { - TypeInfo unifiedType = typeResolver.resolveJSType(currentType); - FieldInfo receiverField = FieldInfo.localField(receiverName, unifiedType, pos, null); - marks.add(new ScriptLine.Mark(pos, pos + receiverName.length(), TokenType.LOCAL_FIELD, receiverField)); - } - - pos += receiverName.length() + 1; // +1 for the dot - - // Walk the chain and mark each member - JSTypeRegistry registry = typeResolver.getJSTypeRegistry(); - for (int i = 1; i < parts.length; i++) { - String member = parts[i]; - int memberStart = pos; - int memberEnd = pos + member.length(); - - if (currentType != null) { - JSTypeInfo jsTypeInfo = registry.getType(currentType); - if (jsTypeInfo != null) { - TypeInfo unifiedContainingType = TypeInfo.fromJSTypeInfo(jsTypeInfo); - - // Check method first - JSMethodInfo jsMethod = jsTypeInfo.getMethod(member); - if (jsMethod != null) { - MethodInfo unifiedMethod = MethodInfo.fromJSMethod(jsMethod, unifiedContainingType); - marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.METHOD_CALL, unifiedMethod)); - currentType = jsMethod.getReturnType(); - } else { - // Check field - JSFieldInfo jsField = jsTypeInfo.getField(member); - if (jsField != null) { - FieldInfo unifiedField = FieldInfo.fromJSField(jsField, unifiedContainingType); - marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.GLOBAL_FIELD, unifiedField)); - currentType = jsField.getType(); - } else { - // Unknown member - mark as undefined - marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.UNDEFINED_VAR, - "Unknown member '" + member + "' on type " + jsTypeInfo.getFullName())); - currentType = null; - } - } - } else { - currentType = null; - } - } - - pos = memberEnd + 1; // +1 for the next dot - } - } - } - - /** - * Mark JavaScript method calls. - */ - private void markJSMethodCalls(List marks) { - Pattern callPattern = Pattern.compile("(\\w+)\\s*\\("); - Matcher m = callPattern.matcher(text); - - while (m.find()) { - if (isExcluded(m.start())) continue; - - String callExpr = m.group(1); - int nameStart = m.start(1); - int nameEnd = m.end(1); - - // Skip if it's preceded by a dot (handled by markJSMemberAccesses) - if (nameStart > 0 && text.charAt(nameStart - 1) == '.') continue; - - // Skip keywords that look like function calls - if (callExpr.equals("if") || callExpr.equals("while") || callExpr.equals("for") || - callExpr.equals("switch") || callExpr.equals("catch") || callExpr.equals("function")) { - continue; - } - - if (typeResolver.isJSHook(callExpr)) { - List sigs = typeResolver.getJSHookSignatures(callExpr); - if (!sigs.isEmpty()) { - JSTypeRegistry.HookSignature sig = sigs.get(0); - TypeInfo paramTypeInfo = typeResolver.resolveJSType(sig.paramType); - List methodParams = new ArrayList<>(); - methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo)); - - MethodInfo hookMethod = MethodInfo.declaration( - callExpr, null, TypeInfo.fromPrimitive("void"), methodParams, - nameStart, nameStart, nameStart, -1, -1, 0, sig.doc - ); - marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, hookMethod)); - } - } - } - } - - /** - * Mark standalone JavaScript identifiers (parameters and variables in function bodies). - */ - private void markJSIdentifiers(List marks) { - // Build set of positions already marked - Set markedPositions = new HashSet<>(); - for (ScriptLine.Mark mark : marks) { - for (int i = mark.start; i < mark.end; i++) { - markedPositions.add(i); - } - } - - // Find all identifiers - Pattern identPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); - Matcher matcher = identPattern.matcher(text); - - while (matcher.find()) { - if (isExcluded(matcher.start())) continue; - - int start = matcher.start(); - int end = matcher.end(); - - // Skip if already marked - boolean alreadyMarked = false; - for (int i = start; i < end; i++) { - if (markedPositions.contains(i)) { - alreadyMarked = true; - break; - } - } - if (alreadyMarked) continue; - - String identifier = matcher.group(); - - // Check if it's a parameter - boolean isParameter = false; - for (Map params : jsFunctionParams.values()) { - if (params.containsKey(identifier)) { - isParameter = true; - break; - } - } - - if (isParameter) { - marks.add(new ScriptLine.Mark(start, end, TokenType.PARAMETER)); - for (int i = start; i < end; i++) { - markedPositions.add(i); - } - } else if (jsVariableTypes.containsKey(identifier)) { - marks.add(new ScriptLine.Mark(start, end, TokenType.LOCAL_FIELD)); - for (int i = start; i < end; i++) { - markedPositions.add(i); - } - } + // Class/interface/enum declarations - Java only + if (!isJavaScript()) { + markClassDeclarations(marks); + markEnumConstants(marks); } - } - - /** - * Build marks specific to Java/Groovy. - */ - private void buildJavaMarks(List marks) { - // Import statements - markImports(marks); - // Class/interface/enum declarations - markClassDeclarations(marks); - - // Enum constants (must be after class declarations so enums are known) - markEnumConstants(marks); - - // Modifiers - addPatternMarks(marks, MODIFIER_PATTERN, TokenType.MODIFIER); + // Modifiers - Java only + if (!isJavaScript()) { + addPatternMarks(marks, MODIFIER_PATTERN, TokenType.MODIFIER); + } - // Type declarations and usages - markTypeDeclarations(marks); + // Type declarations and usages - Java only (JS doesn't have explicit types) + if (!isJavaScript()) { + markTypeDeclarations(marks); + } - // Methods + // Methods/functions - UNIFIED (uses shared 'methods' list) markMethodDeclarations(marks); - // Method calls (parse before variables so we can attach context) + // Method calls - UNIFIED (stores in shared 'methodCalls' list) markMethodCalls(marks); - // Variables and fields + // Variables and fields - UNIFIED (uses shared globalFields, methodLocals) markVariables(marks); - // Chained field accesses (e.g., mc.player.world, this.field) + // Chained field accesses - UNIFIED (uses resolveVariable which handles both) markChainedFieldAccesses(marks); - // Cast type expressions (e.g., (EntityPlayer) entity) - markCastTypes(marks); - - // Imported class usages - markImportedClassUsages(marks); - - // Mark unused imports (after all other marks are built) - markUnusedImports(marks); + // Java-specific final passes + if (!isJavaScript()) { + markCastTypes(marks); + markImportedClassUsages(marks); + markUnusedImports(marks); + } // Final pass: Mark any remaining unmarked identifiers as undefined markUndefinedIdentifiers(marks); + + return marks; } /** @@ -5313,48 +4930,12 @@ FieldAccessInfo createFieldAccessInfo(String name, int start, int end, return accessInfo; } - FieldInfo resolveVariable(String name, int position) { - // For JavaScript, use the JS variable tracking - if (isJavaScript()) { - return resolveJSVariable(name, position); - } - - return resolveJavaVariable(name, position); - } - /** - * Resolve a JavaScript variable by name. + * Resolve a variable by name at a given position. + * Works for BOTH Java and JavaScript using unified data structures. */ - private FieldInfo resolveJSVariable(String name, int position) { - // Check if it's a parameter - for (Map.Entry> entry : jsFunctionParams.entrySet()) { - if (entry.getValue().containsKey(name)) { - String typeName = entry.getValue().get(name); - TypeInfo typeInfo = typeResolver.resolveJSType(typeName); - return FieldInfo.parameter(name, typeInfo, position, null); - } - } - - // Check if it's a tracked variable - if (jsVariableTypes.containsKey(name)) { - String typeName = jsVariableTypes.get(name); - TypeInfo typeInfo = typeResolver.resolveJSType(typeName); - return FieldInfo.localField(name, typeInfo, position, null); - } - - // Check global fields (variables declared with var/let/const) - if (globalFields.containsKey(name)) { - return globalFields.get(name); - } - - return null; - } - - /** - * Resolve a Java variable by name. - */ - private FieldInfo resolveJavaVariable(String name, int position) { - // Find containing method + FieldInfo resolveVariable(String name, int position) { + // Find containing method/function MethodInfo containingMethod = findMethodAtPosition(position); if (containingMethod != null) { @@ -5363,7 +4944,7 @@ private FieldInfo resolveJavaVariable(String name, int position) { return containingMethod.getParameter(name); } - // Check local variables + // Check local variables (methodLocals stores both Java locals and JS var/let/const inside functions) Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); if (locals != null && locals.containsKey(name)) { FieldInfo localInfo = locals.get(name); @@ -5373,13 +4954,15 @@ private FieldInfo resolveJavaVariable(String name, int position) { } } - // Check if we're inside a script type and look for fields there - ScriptTypeInfo enclosingType = findEnclosingScriptType(position); - if (enclosingType != null && enclosingType.hasField(name)) { - return enclosingType.getFieldInfo(name); + // For Java only: Check if we're inside a script type and look for fields there + if (!isJavaScript()) { + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasField(name)) { + return enclosingType.getFieldInfo(name); + } } - // Check global fields + // Check global fields (stores both Java global fields and JS global var/let/const) if (globalFields.containsKey(name)) { return globalFields.get(name); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java index 2a730efda..0ebcce69f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSScriptAnalyzer.java @@ -16,14 +16,16 @@ * Uses the JSTypeRegistry to resolve types and validate member access. * * @deprecated This class is deprecated. All JavaScript analysis is now handled by the - * unified pipeline in {@link ScriptDocument}. The functionality has been merged into: + * unified pipeline in {@link ScriptDocument}. JavaScript and Java scripts now use the + * same methods and data structures: *
    - *
  • {@link ScriptDocument#parseJSFunctions()} - for function parsing
  • - *
  • {@link ScriptDocument#parseJSVariables()} - for variable parsing
  • - *
  • {@link ScriptDocument#buildJSMarks(List)} - for mark building
  • + *
  • {@link ScriptDocument#parseMethodDeclarations()} - for both Java methods and JS functions
  • + *
  • {@link ScriptDocument#parseLocalVariables()} - for both Java and JS local variables
  • + *
  • {@link ScriptDocument#parseGlobalFields()} - for both Java and JS global variables
  • + *
  • {@link ScriptDocument#buildMarks(List)} - for mark building (both languages)
  • *
- * Use {@link ScriptDocument#getJSVariableTypes()} and {@link ScriptDocument#getJSFunctionParams()} - * to access inferred type information. + * Type information is available via the unified data structures: {@code getMethods()}, + * {@code getMethodLocals()}, and {@code getGlobalFields()}. */ @Deprecated public class JSScriptAnalyzer { From bbe6d9bb77aa1d309a415090b140761c909d7204 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 22:41:00 +0200 Subject: [PATCH 198/337] JSAutoCompleteProvier revamp: -Type resolution now happens directly in ScriptDocument -Extends JavaAutocompleteProvider -Removed redundant code/logic --- .../autocomplete/AutocompleteManager.java | 5 +- .../autocomplete/JSAutocompleteProvider.java | 294 +++++++----------- .../JavaAutocompleteProvider.java | 30 +- 3 files changed, 122 insertions(+), 207 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 47c05c286..4abfa173a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -390,10 +390,7 @@ private void showSuggestions(String text, int cursorPosition, String prefix, // Get suggestions from appropriate provider AutocompleteProvider provider = document.isJavaScript() ? jsProvider : javaProvider; - // For JavaScript, update variable types from analyzer - if (document.isJavaScript() && document.getJSAnalyzer() != null) { - jsProvider.updateVariableTypes(document.getJSAnalyzer().getVariableTypes()); - } + // No need to update variable types - JSAutocompleteProvider now gets them directly from ScriptDocument List suggestions = provider.getSuggestions(context); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index 3fb105d7a..bf44aedab 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -1,22 +1,21 @@ package noppes.npcs.client.gui.util.script.autocomplete; import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.*; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.*; /** * Autocomplete provider for JavaScript/ECMAScript scripts. - * Uses JSTypeRegistry for type resolution. + * Uses ScriptDocument's unified TypeInfo/TypeResolver system for type resolution. */ -public class JSAutocompleteProvider implements AutocompleteProvider { +public class JSAutocompleteProvider extends JavaAutocompleteProvider { - private ScriptDocument document; private final JSTypeRegistry registry; - // Variable types inferred from the document - private Map variableTypes = new HashMap<>(); - public JSAutocompleteProvider() { this.registry = JSTypeRegistry.getInstance(); if (!registry.isInitialized()) { @@ -24,17 +23,6 @@ public JSAutocompleteProvider() { } } - public void setDocument(ScriptDocument document) { - this.document = document; - } - - /** - * Update the variable type map from document analysis. - */ - public void updateVariableTypes(Map types) { - this.variableTypes = types != null ? new HashMap<>(types) : new HashMap<>(); - } - @Override public boolean canProvide(Context context) { return document != null && document.isJavaScript(); @@ -43,13 +31,13 @@ public boolean canProvide(Context context) { @Override public List getSuggestions(Context context) { List items = new ArrayList<>(); - - // Resolve owner type for usage tracking + + // Resolve owner type for usage tracking using ScriptDocument's unified type resolution String ownerFullName = null; if (context.isMemberAccess && context.receiverExpression != null) { - String receiverType = resolveReceiverType(context.receiverExpression); - if (receiverType != null) { - ownerFullName = receiverType; + TypeInfo receiverType = document.resolveExpressionType(context.receiverExpression, context.prefixStart); + if (receiverType != null && receiverType.isResolved()) { + ownerFullName = receiverType.getFullName(); } } @@ -62,7 +50,7 @@ public List getSuggestions(Context context) { } // Filter and score by prefix, then apply usage boosts - filterAndScore(items, context.prefix, context.isMemberAccess, ownerFullName); + filterAndScore(items, context.prefix, context.isMemberAccess, false, ownerFullName); // Sort by score Collections.sort(items); @@ -77,35 +65,75 @@ public List getSuggestions(Context context) { /** * Add suggestions for member access (after dot). + * Uses ScriptDocument's unified type resolution system. */ - private void addMemberSuggestions(Context context, List items) { + protected void addMemberSuggestions(Context context, List items) { String receiverExpr = context.receiverExpression; if (receiverExpr == null || receiverExpr.isEmpty()) { return; } - - // Infer type of receiver - String receiverType = inferExpressionType(receiverExpr); - if (receiverType == null) { + + // Use ScriptDocument's resolveExpressionType - handles both Java and JS + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart); + if (receiverType == null || !receiverType.isResolved()) { return; } - - JSTypeInfo typeInfo = registry.getType(receiverType); - if (typeInfo == null) { + + // For JS types, check JSTypeRegistry + JSTypeInfo jsTypeInfo = registry.getType(receiverType.getSimpleName()); + if (jsTypeInfo != null) { + // Add methods + addMethodsFromType(jsTypeInfo, items, new HashSet<>()); + // Add fields + addFieldsFromType(jsTypeInfo, items, new HashSet<>()); return; } - - // Add methods - addMethodsFromType(typeInfo, items, new HashSet<>()); - - // Add fields - addFieldsFromType(typeInfo, items, new HashSet<>()); + + // For Java types with Java class, use reflection to get members + Class javaClass = receiverType.getJavaClass(); + if (javaClass != null) { + try { + // Add methods from Java reflection + for (java.lang.reflect.Method method : javaClass.getMethods()) { + String methodName = method.getName(); + StringBuilder signature = new StringBuilder(); + signature.append(methodName).append("("); + Class[] params = method.getParameterTypes(); + for (int i = 0; i < params.length; i++) { + if (i > 0) + signature.append(", "); + signature.append(params[i].getSimpleName()); + } + signature.append(")"); + + items.add(new AutocompleteItem.Builder() + .name(methodName) + .insertText(methodName) + .kind(AutocompleteItem.Kind.METHOD) + .typeLabel(method.getReturnType().getSimpleName()) + .signature(signature.toString()) + .build()); + } + + // Add fields from Java reflection + for (java.lang.reflect.Field field : javaClass.getFields()) { + items.add(new AutocompleteItem.Builder() + .name(field.getName()) + .insertText(field.getName()) + .kind(AutocompleteItem.Kind.FIELD) + .typeLabel(field.getType().getSimpleName()) + .build()); + } + } catch (SecurityException e) { + // Can't access members, skip + } + } } /** * Recursively add methods from a type and its parents. */ - private void addMethodsFromType(JSTypeInfo type, List items, Set added) { + protected void addMethodsFromType(JSTypeInfo type, List items, Set added) { for (JSMethodInfo method : type.getMethods().values()) { String name = method.getName(); // Skip overload markers (name$1, name$2, etc.) @@ -127,7 +155,7 @@ private void addMethodsFromType(JSTypeInfo type, List items, S /** * Recursively add fields from a type and its parents. */ - private void addFieldsFromType(JSTypeInfo type, List items, Set added) { + protected void addFieldsFromType(JSTypeInfo type, List items, Set added) { for (JSFieldInfo field : type.getFields().values()) { if (!added.contains(field.getName())) { added.add(field.getName()); @@ -143,131 +171,55 @@ private void addFieldsFromType(JSTypeInfo type, List items, Se /** * Add suggestions based on current scope (not after a dot). + * Uses ScriptDocument's unified data structures for variables and functions. */ - private void addScopeSuggestions(Context context, List items) { + protected void addScopeSuggestions(Context context, List items) { + + int pos = context.cursorPosition; + + // Find containing method + MethodInfo containingMethod = document.findContainingMethod(pos); + // Add local variables - for (Map.Entry entry : variableTypes.entrySet()) { - String varName = entry.getKey(); - String varType = entry.getValue(); - - items.add(new AutocompleteItem.Builder() - .name(varName) - .insertText(varName) - .kind(AutocompleteItem.Kind.VARIABLE) - .typeLabel(varType != null ? varType : "any") - .build()); - } - - // Add hook functions - for (String hookName : registry.getHookNames()) { - List sigs = registry.getHookSignatures(hookName); - if (!sigs.isEmpty()) { - JSTypeRegistry.HookSignature sig = sigs.get(0); - items.add(new AutocompleteItem.Builder() - .name(hookName) - .insertText(hookName) - .kind(AutocompleteItem.Kind.METHOD) - .typeLabel("hook") - .signature("function " + hookName + "(e: " + sig.paramType + ")") - .documentation(sig.doc) - .build()); + if (containingMethod != null) { + Map locals = document.getLocalsForMethod(containingMethod); + if (locals != null) { + for (FieldInfo local : locals.values()) { + if (local.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(local)); + } + } } - } - - // Add global types - for (String typeName : registry.getTypeNames()) { - JSTypeInfo type = registry.getType(typeName); - if (type != null) { - items.add(new AutocompleteItem.Builder() - .name(typeName) - .insertText(typeName) - .kind(AutocompleteItem.Kind.CLASS) - .typeLabel("interface") - .build()); + + // Add method parameters + for (FieldInfo param : containingMethod.getParameters()) { + items.add(AutocompleteItem.fromField(param)); } } - - // Add JavaScript keywords - addJSKeywords(items); - } - - /** - * Infer the type of an expression. - */ - private String inferExpressionType(String expr) { - return resolveReceiverType(expr); - } - - /** - * Resolve the type of a receiver expression for member access. - */ - private String resolveReceiverType(String expr) { - if (expr == null || expr.isEmpty()) { - return null; - } - - expr = expr.trim(); - - // Check if it's a known variable - if (variableTypes.containsKey(expr)) { - return variableTypes.get(expr); - } - - // Handle method chain: x.y.z() -> resolve step by step - if (expr.contains(".")) { - String[] parts = expr.split("\\."); - String currentType = null; - - // Start with the first part - if (variableTypes.containsKey(parts[0])) { - currentType = variableTypes.get(parts[0]); - } - - if (currentType == null) { - return null; - } - - // Walk through the chain - for (int i = 1; i < parts.length; i++) { - String member = parts[i]; - // Remove () for method calls - if (member.endsWith("()")) { - member = member.substring(0, member.length() - 2); - } - - JSTypeInfo typeInfo = registry.getType(currentType); - if (typeInfo == null) { - return null; - } - - // Check method - JSMethodInfo method = typeInfo.getMethod(member); - if (method != null) { - currentType = method.getReturnType(); - continue; - } - - // Check field - JSFieldInfo field = typeInfo.getField(member); - if (field != null) { - currentType = field.getType(); - continue; - } - - // Unknown member - return null; + + // Add global fields + for (FieldInfo globalField : document.getGlobalFields().values()) { + if (globalField.isVisibleAt(pos)) { + items.add(AutocompleteItem.fromField(globalField)); } + } + + // Add script-defined methods + for (MethodInfo method : document.getAllMethods()) { + items.add(AutocompleteItem.fromMethod(method)); - return currentType; } - return null; + // Add JavaScript keywords + addKeywords(items); } + + /** * Add JavaScript keywords. */ - private void addJSKeywords(List items) { + protected void addKeywords(List items) { String[] keywords = { "function", "var", "let", "const", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "return", "try", "catch", "finally", @@ -280,44 +232,8 @@ private void addJSKeywords(List items) { items.add(AutocompleteItem.keyword(keyword)); } } - - /** - * Filter items by prefix, calculate match scores, and apply usage boosts. - */ - private void filterAndScore(List items, String prefix, - boolean isMemberAccess, String ownerFullName) { - UsageTracker tracker = UsageTracker.getJSInstance(); - - if (prefix == null || prefix.isEmpty()) { - for (AutocompleteItem item : items) { - item.calculateMatchScore(""); - applyUsageBoost(item, tracker, ownerFullName); - } - return; - } - - // For non-member access (first word), require strict prefix matching - // For member access (after dot), allow fuzzy/contains matching - boolean requirePrefix = !isMemberAccess; - - Iterator iter = items.iterator(); - while (iter.hasNext()) { - AutocompleteItem item = iter.next(); - int score = item.calculateMatchScore(prefix, requirePrefix); - if (score < 0) { - iter.remove(); - } else { - applyUsageBoost(item, tracker, ownerFullName); - } - } - } - - /** - * Apply usage-based score boost to an item. - */ - private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) { - int usageCount = tracker.getUsageCount(item, ownerFullName); - int boost = UsageTracker.calculateUsageBoost(usageCount); - item.addScoreBoost(boost); + + protected UsageTracker getUsageTracker() { + return UsageTracker.getJSInstance(); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index d4f59926c..c0a897403 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -19,7 +19,7 @@ */ public class JavaAutocompleteProvider implements AutocompleteProvider { - private ScriptDocument document; + protected ScriptDocument document; public void setDocument(ScriptDocument document) { this.document = document; @@ -71,7 +71,7 @@ public List getSuggestions(Context context) { /** * Add suggestions for member access (after dot). */ - private void addMemberSuggestions(Context context, List items) { + protected void addMemberSuggestions(Context context, List items) { String receiverExpr = context.receiverExpression; if (receiverExpr == null || receiverExpr.isEmpty()) { return; @@ -132,7 +132,7 @@ private void addMemberSuggestions(Context context, List items) /** * Add members from a script-defined type. */ - private void addScriptTypeMembers(ScriptTypeInfo scriptType, List items, boolean isStaticContext) { + protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List items, boolean isStaticContext) { // Add methods (getMethods returns Map>) for (List overloads : scriptType.getMethods().values()) { for (MethodInfo method : overloads) { @@ -181,7 +181,7 @@ private void addScriptTypeMembers(ScriptTypeInfo scriptType, List items) { + protected void addScopeSuggestions(Context context, List items) { int pos = context.cursorPosition; // Find containing method @@ -256,7 +256,7 @@ private void addScopeSuggestions(Context context, List items) * Add suggestions for unimported classes that match the prefix. * These will trigger auto-import when selected. */ - private void addUnimportedClassSuggestions(String prefix, List items) { + protected void addUnimportedClassSuggestions(String prefix, List items) { // Get the type resolver TypeResolver resolver = TypeResolver.getInstance(); // Find classes matching this prefix (not just exact matches) @@ -312,7 +312,7 @@ private void addUnimportedClassSuggestions(String prefix, List /** * Add Java keywords. */ - private void addKeywords(List items) { + protected void addKeywords(List items) { String[] keywords = { "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "return", "try", "catch", "finally", "throw", "throws", "new", "this", "super", @@ -331,7 +331,7 @@ private void addKeywords(List items) { * Find the enclosing script type at a position. * This is a workaround since findEnclosingScriptType is package-private. */ - private ScriptTypeInfo findEnclosingType(int position) { + protected ScriptTypeInfo findEnclosingType(int position) { for (ScriptTypeInfo type : document.getScriptTypesMap().values()) { if (type.containsPosition(position)) { return type; @@ -344,18 +344,20 @@ private ScriptTypeInfo findEnclosingType(int position) { * Check if the receiver expression represents static access (class type). * Similar logic to FieldChainMarker.isStaticContext(). */ - private boolean isStaticAccess(String receiverExpr, int position) { + protected boolean isStaticAccess(String receiverExpr, int position) { // Try to resolve the receiver expression as a type TypeInfo typeCheck = document.resolveType(receiverExpr); return typeCheck != null && typeCheck.isResolved(); } - + protected UsageTracker getUsageTracker() { + return UsageTracker.getJavaInstance(); + } /** * Filter items by prefix, calculate match scores, apply usage boosts, and penalize static members in instance contexts. */ - private void filterAndScore(List items, String prefix, + protected void filterAndScore(List items, String prefix, boolean isMemberAccess, boolean isStaticContext, String ownerFullName) { - UsageTracker tracker = UsageTracker.getJavaInstance(); + UsageTracker tracker = getUsageTracker(); if (prefix == null || prefix.isEmpty()) { // No filtering needed, all items get a base score + usage boost @@ -390,7 +392,7 @@ private void filterAndScore(List items, String prefix, /** * Apply usage-based score boost to an item. */ - private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) { + protected void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) { int usageCount = tracker.getUsageCount(item, ownerFullName); int boost = UsageTracker.calculateUsageBoost(usageCount); item.addScoreBoost(boost); @@ -402,7 +404,7 @@ private void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String * accessing through an instance (e.g., Minecraft.getMinecraft().getMinecraft()). * However, if the item is a very strong match (exact prefix), don't penalize as much. */ - private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) { + protected void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) { // Only apply penalty in member access contexts (after dot) if (!isMemberAccess) { return; @@ -434,7 +436,7 @@ private void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, b * Apply penalty to inherited Object methods to push them to bottom. * Strong matches get lighter penalty, weak matches get pushed all the way down. */ - private void applyObjectMethodPenalty(AutocompleteItem item) { + protected void applyObjectMethodPenalty(AutocompleteItem item) { if (!item.isInheritedObjectMethod()) { return; } From b267ebecb27fd8a3ecb08d8af5fb76560cf4c483 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 22:48:23 +0200 Subject: [PATCH 199/337] More JSAutocompleteProvider cleanup --- .../autocomplete/JSAutocompleteProvider.java | 107 ++---------------- .../JavaAutocompleteProvider.java | 33 +++--- .../script/interpreter/type/TypeChecker.java | 25 ++++ 3 files changed, 53 insertions(+), 112 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index bf44aedab..a068bfaf0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -4,6 +4,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.*; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.*; @@ -28,40 +29,6 @@ public boolean canProvide(Context context) { return document != null && document.isJavaScript(); } - @Override - public List getSuggestions(Context context) { - List items = new ArrayList<>(); - - // Resolve owner type for usage tracking using ScriptDocument's unified type resolution - String ownerFullName = null; - if (context.isMemberAccess && context.receiverExpression != null) { - TypeInfo receiverType = document.resolveExpressionType(context.receiverExpression, context.prefixStart); - if (receiverType != null && receiverType.isResolved()) { - ownerFullName = receiverType.getFullName(); - } - } - - if (context.isMemberAccess) { - // Member access: resolve type of receiver and get its members - addMemberSuggestions(context, items); - } else { - // Identifier context: show variables, functions, types in scope - addScopeSuggestions(context, items); - } - - // Filter and score by prefix, then apply usage boosts - filterAndScore(items, context.prefix, context.isMemberAccess, false, ownerFullName); - - // Sort by score - Collections.sort(items); - - // Limit results - if (items.size() > 50) { - items = items.subList(0, 50); - } - - return items; - } /** * Add suggestions for member access (after dot). @@ -80,7 +47,7 @@ protected void addMemberSuggestions(Context context, List item } // For JS types, check JSTypeRegistry - JSTypeInfo jsTypeInfo = registry.getType(receiverType.getSimpleName()); + JSTypeInfo jsTypeInfo = receiverType.getJSTypeInfo(); if (jsTypeInfo != null) { // Add methods addMethodsFromType(jsTypeInfo, items, new HashSet<>()); @@ -168,72 +135,18 @@ protected void addFieldsFromType(JSTypeInfo type, List items, addFieldsFromType(type.getResolvedParent(), items, added); } } - - /** - * Add suggestions based on current scope (not after a dot). - * Uses ScriptDocument's unified data structures for variables and functions. - */ - protected void addScopeSuggestions(Context context, List items) { - int pos = context.cursorPosition; - - // Find containing method - MethodInfo containingMethod = document.findContainingMethod(pos); - - // Add local variables - if (containingMethod != null) { - Map locals = document.getLocalsForMethod(containingMethod); - if (locals != null) { - for (FieldInfo local : locals.values()) { - if (local.isVisibleAt(pos)) { - items.add(AutocompleteItem.fromField(local)); - } - } - } - - // Add method parameters - for (FieldInfo param : containingMethod.getParameters()) { - items.add(AutocompleteItem.fromField(param)); - } - } - - // Add global fields - for (FieldInfo globalField : document.getGlobalFields().values()) { - if (globalField.isVisibleAt(pos)) { - items.add(AutocompleteItem.fromField(globalField)); - } - } - - // Add script-defined methods - for (MethodInfo method : document.getAllMethods()) { - items.add(AutocompleteItem.fromMethod(method)); - - } - - // Add JavaScript keywords - addKeywords(items); - } - - - - /** - * Add JavaScript keywords. - */ - protected void addKeywords(List items) { - String[] keywords = { - "function", "var", "let", "const", "if", "else", "for", "while", "do", - "switch", "case", "break", "continue", "return", "try", "catch", "finally", - "throw", "new", "typeof", "instanceof", "in", "of", "this", "null", - "undefined", "true", "false", "async", "await", "yield", "class", "extends", - "import", "export", "default" - }; - - for (String keyword : keywords) { - items.add(AutocompleteItem.keyword(keyword)); - } + protected void addUnimportedClassSuggestions(String prefix, List items) { + // For JavaScript, we typically don't suggest unimported classes + // as imports are handled differently. This can be customized + // if needed to suggest global JS types. } protected UsageTracker getUsageTracker() { return UsageTracker.getJSInstance(); } + + public String[] getKeywords() { + return TypeChecker.getJaveScriptKeywords(); + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index c0a897403..e60e874eb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -5,6 +5,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; @@ -232,26 +233,31 @@ protected void addScopeSuggestions(Context context, List items for (MethodInfo method : document.getAllMethods()) { items.add(AutocompleteItem.fromMethod(method)); } + + addLanguageUniqueSuggestions(context, items); + // Add keywords + addKeywords(items); + } + + protected void addLanguageUniqueSuggestions(Context context, List items) { // Add imported types for (TypeInfo type : document.getImportedTypes()) { items.add(AutocompleteItem.fromType(type)); } - + // Add script-defined types for (ScriptTypeInfo scriptType : document.getScriptTypesMap().values()) { items.add(AutocompleteItem.fromType(scriptType)); } - + // Add unimported classes that match the prefix (for auto-import) if (context.prefix != null && context.prefix.length() >= 2 && Character.isUpperCase(context.prefix.charAt(0))) { addUnimportedClassSuggestions(context.prefix, items); } - - // Add keywords - addKeywords(items); } + /** * Add suggestions for unimported classes that match the prefix. * These will trigger auto-import when selected. @@ -313,19 +319,14 @@ protected void addUnimportedClassSuggestions(String prefix, List items) { - String[] keywords = { - "if", "else", "for", "while", "do", "switch", "case", "break", "continue", - "return", "try", "catch", "finally", "throw", "throws", "new", "this", "super", - "true", "false", "null", "instanceof", "import", "class", "interface", "enum", - "extends", "implements", "public", "private", "protected", "static", "final", - "abstract", "synchronized", "volatile", "transient", "native", "void", - "boolean", "byte", "short", "int", "long", "float", "double", "char" - }; - - for (String keyword : keywords) { + for (String keyword : getKeywords()) { items.add(AutocompleteItem.keyword(keyword)); } } + + public String[] getKeywords() { + return TypeChecker.getJavaKeywords(); + } /** * Find the enclosing script type at a position. @@ -349,9 +350,11 @@ protected boolean isStaticAccess(String receiverExpr, int position) { TypeInfo typeCheck = document.resolveType(receiverExpr); return typeCheck != null && typeCheck.isResolved(); } + protected UsageTracker getUsageTracker() { return UsageTracker.getJavaInstance(); } + /** * Filter items by prefix, calculate match scores, apply usage boosts, and penalize static members in instance contexts. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index ac0aa901d..ed3a573fc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -312,5 +312,30 @@ private static Class getWrapperClassInternal(Class primitive) { if (primitive == double.class) return Double.class; return null; } + + public static String[] getJavaKeywords() { + String[] keywords = { + "if", "else", "for", "while", "do", "switch", "case", "break", "continue", + "return", "try", "catch", "finally", "throw", "throws", "new", "this", "super", + "true", "false", "null", "instanceof", "import", "class", "interface", "enum", + "extends", "implements", "public", "private", "protected", "static", "final", + "abstract", "synchronized", "volatile", "transient", "native", "void", + "boolean", "byte", "short", "int", "long", "float", "double", "char" + }; + return keywords; + } + + public static String[] getJaveScriptKeywords() { + String[] keywords = { + "function", "var", "let", "const", "if", "else", "for", "while", "do", + "switch", "case", "break", "continue", "return", "try", "catch", "finally", + "throw", "new", "typeof", "instanceof", "in", "of", "this", "null", + "undefined", "true", "false", "async", "await", "yield", "class", "extends", + "import", "export", "default" + }; + + + return keywords; + } } From f8802114b5f2ff207d539f3dc3acbcdce1a5f938 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 23:33:56 +0200 Subject: [PATCH 200/337] Fixed indexOutOfbounds crash --- .../client/gui/util/GuiScriptTextArea.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) 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 f49547c2e..2950b08f9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -481,21 +481,33 @@ public int getViewportWidth() { @Override public void insertText(String text, int startPosition) { // Replace text from startPosition to current cursor + String fullText = GuiScriptTextArea.this.text; int cursorPos = selection.getCursorPosition(); - String before = GuiScriptTextArea.this.text.substring(0, startPosition); - String after = GuiScriptTextArea.this.text.substring(cursorPos); + + // Bounds check to prevent StringIndexOutOfBoundsException + int start = Math.max(0, Math.min(startPosition, fullText.length())); + int cursor = Math.max(start, Math.min(cursorPos, fullText.length())); + + String before = fullText.substring(0, start); + String after = fullText.substring(cursor); setText(before + text + after); - selection.reset(startPosition + text.length()); + selection.reset(start + text.length()); scrollToCursor(); } @Override public void replaceTextRange(String text, int startPosition, int endPosition) { // Replace text from startPosition to endPosition - String before = GuiScriptTextArea.this.text.substring(0, startPosition); - String after = GuiScriptTextArea.this.text.substring(endPosition); + String fullText = GuiScriptTextArea.this.text; + + // Bounds check to prevent StringIndexOutOfBoundsException + int start = Math.max(0, Math.min(startPosition, fullText.length())); + int end = Math.max(start, Math.min(endPosition, fullText.length())); + + String before = fullText.substring(0, start); + String after = fullText.substring(end); setText(before + text + after); - selection.reset(startPosition + text.length()); + selection.reset(start + text.length()); scrollToCursor(); } From eea82a6c473e338fe45527317cea8ad0a1f04d06 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 4 Jan 2026 23:35:59 +0200 Subject: [PATCH 201/337] Recursively loaded all .d.ts files from assets/customnpcs/api/ --- .../interpreter/js_parser/JSTypeRegistry.java | 199 ++++++++++-------- 1 file changed, 115 insertions(+), 84 deletions(-) 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 a724b5d2e..93e9f656b 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 @@ -4,7 +4,10 @@ import net.minecraft.util.ResourceLocation; import java.io.*; +import java.net.URL; import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; /** * Central registry for all TypeScript types parsed from .d.ts files. @@ -46,7 +49,7 @@ private JSTypeRegistry() {} /** * Initialize the registry from the embedded resources. - * This loads .d.ts files from assets/customnpcs/api/ + * Recursively loads all .d.ts files from assets/customnpcs/api/ */ public void initializeFromResources() { if (initialized || initializationAttempted) return; @@ -55,20 +58,28 @@ public void initializeFromResources() { try { TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); - // Load hooks.d.ts first (defines function parameter types) - loadResourceFile(parser, "hooks.d.ts"); + // Recursively load all .d.ts files from the api directory + Set dtsFiles = findAllDtsFilesInResources("assets/customnpcs/api"); - // Load index.d.ts (type aliases) - loadResourceFile(parser, "index.d.ts"); + System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files in resources"); - // Load main API files - loadResourceDirectory(parser, "noppes/npcs/api/"); - loadResourceDirectory(parser, "noppes/npcs/api/entity/"); - loadResourceDirectory(parser, "noppes/npcs/api/event/"); - loadResourceDirectory(parser, "noppes/npcs/api/handler/"); - loadResourceDirectory(parser, "noppes/npcs/api/item/"); - loadResourceDirectory(parser, "noppes/npcs/api/block/"); - loadResourceDirectory(parser, "noppes/npcs/api/gui/"); + // Load hooks.d.ts and index.d.ts first if they exist (defines core types) + if (dtsFiles.contains("api/hooks.d.ts")) { + loadResourceFile(parser, "hooks.d.ts"); + } + if (dtsFiles.contains("api/index.d.ts")) { + loadResourceFile(parser, "index.d.ts"); + } + + // Load all other .d.ts files + for (String filePath : dtsFiles) { + if (filePath.startsWith("api/")) { + String relPath = filePath.substring(4); // Remove "api/" prefix + if (!relPath.equals("hooks.d.ts") && !relPath.equals("index.d.ts")) { + loadResourceFile(parser, relPath); + } + } + } resolveInheritance(); initialized = true; @@ -79,6 +90,97 @@ public void initializeFromResources() { } } + /** + * Recursively find all .d.ts files in the resources directory. + * Similar to ClassIndex.addPackage, scans both file system and JAR resources. + */ + private Set findAllDtsFilesInResources(String basePath) { + Set dtsFiles = new HashSet<>(); + + try { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration resources = classLoader.getResources(basePath); + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + + if (resource.getProtocol().equals("file")) { + // Scan file system directory + File directory = new File(resource.getFile()); + scanDirectoryForDts(directory, basePath, dtsFiles); + } else if (resource.getProtocol().equals("jar")) { + // Scan JAR file + String jarPath = resource.getPath(); + if (jarPath.startsWith("file:")) { + jarPath = jarPath.substring(5); + } + int separatorIndex = jarPath.indexOf("!"); + if (separatorIndex != -1) { + jarPath = jarPath.substring(0, separatorIndex); + } + scanJarForDts(jarPath, basePath, dtsFiles); + } + } + } catch (Exception e) { + System.err.println("[JSTypeRegistry] Error scanning for .d.ts files: " + e.getMessage()); + } + + return dtsFiles; + } + + /** + * Recursively scan a file system directory for .d.ts files. + */ + private void scanDirectoryForDts(File directory, String basePath, Set dtsFiles) { + if (!directory.exists() || !directory.isDirectory()) { + return; + } + + File[] files = directory.listFiles(); + if (files == null) { + return; + } + + for (File file : files) { + String fileName = file.getName(); + + if (file.isDirectory()) { + // Recursively scan subdirectory + scanDirectoryForDts(file, basePath + "/" + fileName, dtsFiles); + } else if (fileName.endsWith(".d.ts")) { + // Add .d.ts file path relative to assets/customnpcs/ + dtsFiles.add(basePath + "/" + fileName); + } + } + } + + /** + * Scan a JAR file for .d.ts files in the specified base path. + */ + private void scanJarForDts(String jarPath, String basePath, Set dtsFiles) { + try { + JarFile jarFile = new JarFile(jarPath); + Enumeration entries = jarFile.entries(); + String searchPath = "assets/customnpcs/" + basePath; + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + // Check if entry is under our base path and is a .d.ts file + if (entryName.startsWith(searchPath) && entryName.endsWith(".d.ts")) { + // Convert to relative path from assets/customnpcs/ + String relativePath = entryName.substring("assets/customnpcs/".length()); + dtsFiles.add(relativePath); + } + } + + jarFile.close(); + } catch (Exception e) { + System.err.println("[JSTypeRegistry] Error scanning JAR for .d.ts files: " + e.getMessage()); + } + } + /** * Load a specific .d.ts file from resources. */ @@ -102,77 +204,6 @@ private void loadResourceFile(TypeScriptDefinitionParser parser, String fileName } } - /** - * Load all .d.ts files from a resource directory. - */ - private void loadResourceDirectory(TypeScriptDefinitionParser parser, String dirPath) { - // In Minecraft resources, we can't list directory contents directly - // So we'll try to load known files from a manifest or just try common patterns - // For now, let's try loading specific known files - String[] knownFiles = getKnownFilesForDirectory(dirPath); - for (String file : knownFiles) { - loadResourceFile(parser, dirPath + file); - } - } - - /** - * Get list of known .d.ts files for a directory. - * This is a workaround since we can't list resource directories. - */ - private String[] getKnownFilesForDirectory(String dirPath) { - // These are the known API files based on the actual file structure - if (dirPath.endsWith("noppes/npcs/api/")) { - return new String[]{ - "AbstractNpcAPI.d.ts", "IBlock.d.ts", "ICommand.d.ts", "IContainer.d.ts", - "IDamageSource.d.ts", "INbt.d.ts", "IParticle.d.ts", "IPixelmonPlayerData.d.ts", - "IPos.d.ts", "IScreenSize.d.ts", "ISkinOverlay.d.ts", "ITileEntity.d.ts", - "ITimers.d.ts", "IWorld.d.ts" - }; - } else if (dirPath.endsWith("entity/")) { - return new String[]{ - "ICustomNpc.d.ts", "IEntity.d.ts", "IEntityItem.d.ts", "IEntityLiving.d.ts", - "IEntityLivingBase.d.ts", "IPlayer.d.ts", "IProjectile.d.ts", "IAnimal.d.ts", - "IAnimatable.d.ts", "IArrow.d.ts", "IDBCPlayer.d.ts", "IFishHook.d.ts", - "IMonster.d.ts", "IPixelmon.d.ts", "IThrowable.d.ts", "IVillager.d.ts" - }; - } else if (dirPath.endsWith("event/")) { - return new String[]{ - "INpcEvent.d.ts", "IPlayerEvent.d.ts", "IItemEvent.d.ts", "IBlockEvent.d.ts", - "IDialogEvent.d.ts", "IQuestEvent.d.ts", "ICustomGuiEvent.d.ts", - "IAnimationEvent.d.ts", "ICustomNPCsEvent.d.ts", "IFactionEvent.d.ts", - "IForgeEvent.d.ts", "ILinkedItemEvent.d.ts", "IPartyEvent.d.ts", - "IProjectileEvent.d.ts", "IRecipeEvent.d.ts" - }; - } else if (dirPath.endsWith("handler/")) { - return new String[]{ - "ICloneHandler.d.ts", "IDialogHandler.d.ts", "IFactionHandler.d.ts", - "IQuestHandler.d.ts", "IRecipeHandler.d.ts", "ITagHandler.d.ts", - "IAnimationHandler.d.ts", "IActionManager.d.ts", "IAttributeHandler.d.ts", - "ICustomEffectHandler.d.ts", "IMagicHandler.d.ts", "INaturalSpawnsHandler.d.ts", - "IOverlayHandler.d.ts", "IPartyHandler.d.ts", "IPlayerBankData.d.ts", - "IPlayerData.d.ts", "IPlayerDialogData.d.ts", "IPlayerFactionData.d.ts", - "IPlayerItemGiverData.d.ts", "IPlayerMailData.d.ts", "IPlayerQuestData.d.ts", - "IPlayerTransportData.d.ts", "IProfileHandler.d.ts", "ITransportHandler.d.ts" - }; - } else if (dirPath.endsWith("item/")) { - return new String[]{ - "IItemStack.d.ts", "IItemArmor.d.ts", "IItemBook.d.ts", "IItemBlock.d.ts", - "IItemCustom.d.ts", "IItemCustomizable.d.ts", "IItemLinked.d.ts" - }; - } else if (dirPath.endsWith("block/")) { - return new String[]{ - "IBlockScripted.d.ts", "ITextPlane.d.ts" - }; - } else if (dirPath.endsWith("gui/")) { - return new String[]{ - "IButton.d.ts", "ICustomGui.d.ts", "ILabel.d.ts", "ITextField.d.ts", - "ITexturedRect.d.ts", "IScroll.d.ts", "IItemSlot.d.ts", "ICustomGuiComponent.d.ts", - "ILine.d.ts" - }; - } - return new String[]{}; - } - /** * Initialize the registry from a directory containing .d.ts files. */ From 5f4bc813db94fb0943e19236a5926086fab3d956 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 01:11:27 +0200 Subject: [PATCH 202/337] Umm forgot some stuff from last commit --- .../interpreter/js_parser/JSTypeRegistry.java | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) 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 93e9f656b..db985e9fe 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 @@ -64,20 +64,17 @@ public void initializeFromResources() { System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files in resources"); // Load hooks.d.ts and index.d.ts first if they exist (defines core types) - if (dtsFiles.contains("api/hooks.d.ts")) { + if (dtsFiles.contains("hooks.d.ts")) { loadResourceFile(parser, "hooks.d.ts"); } - if (dtsFiles.contains("api/index.d.ts")) { + if (dtsFiles.contains("index.d.ts")) { loadResourceFile(parser, "index.d.ts"); } // Load all other .d.ts files for (String filePath : dtsFiles) { - if (filePath.startsWith("api/")) { - String relPath = filePath.substring(4); // Remove "api/" prefix - if (!relPath.equals("hooks.d.ts") && !relPath.equals("index.d.ts")) { - loadResourceFile(parser, relPath); - } + if (!filePath.equals("hooks.d.ts") && !filePath.equals("index.d.ts")) { + loadResourceFile(parser, filePath); } } @@ -93,13 +90,15 @@ public void initializeFromResources() { /** * Recursively find all .d.ts files in the resources directory. * Similar to ClassIndex.addPackage, scans both file system and JAR resources. + * + * @param resourcePath The full resource path (e.g., "assets/customnpcs/api") */ - private Set findAllDtsFilesInResources(String basePath) { + private Set findAllDtsFilesInResources(String resourcePath) { Set dtsFiles = new HashSet<>(); try { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - Enumeration resources = classLoader.getResources(basePath); + Enumeration resources = classLoader.getResources(resourcePath); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); @@ -107,7 +106,7 @@ private Set findAllDtsFilesInResources(String basePath) { if (resource.getProtocol().equals("file")) { // Scan file system directory File directory = new File(resource.getFile()); - scanDirectoryForDts(directory, basePath, dtsFiles); + scanDirectoryForDts(directory, "", dtsFiles); } else if (resource.getProtocol().equals("jar")) { // Scan JAR file String jarPath = resource.getPath(); @@ -118,7 +117,7 @@ private Set findAllDtsFilesInResources(String basePath) { if (separatorIndex != -1) { jarPath = jarPath.substring(0, separatorIndex); } - scanJarForDts(jarPath, basePath, dtsFiles); + scanJarForDts(jarPath, resourcePath, dtsFiles); } } } catch (Exception e) { @@ -130,8 +129,12 @@ private Set findAllDtsFilesInResources(String basePath) { /** * Recursively scan a file system directory for .d.ts files. + * + * @param directory The directory to scan + * @param currentPath The relative path from the base (used for building file paths) + * @param dtsFiles The set to collect .d.ts file paths */ - private void scanDirectoryForDts(File directory, String basePath, Set dtsFiles) { + private void scanDirectoryForDts(File directory, String currentPath, Set dtsFiles) { if (!directory.exists() || !directory.isDirectory()) { return; } @@ -143,34 +146,34 @@ private void scanDirectoryForDts(File directory, String basePath, Set dt for (File file : files) { String fileName = file.getName(); + String filePath = currentPath.isEmpty() ? fileName : currentPath + "/" + fileName; if (file.isDirectory()) { // Recursively scan subdirectory - scanDirectoryForDts(file, basePath + "/" + fileName, dtsFiles); + scanDirectoryForDts(file, filePath, dtsFiles); } else if (fileName.endsWith(".d.ts")) { - // Add .d.ts file path relative to assets/customnpcs/ - dtsFiles.add(basePath + "/" + fileName); + // Add .d.ts file path + dtsFiles.add(filePath); } } } /** - * Scan a JAR file for .d.ts files in the specified base path. + * Scan a JAR file for .d.ts files in the specified resource path. */ - private void scanJarForDts(String jarPath, String basePath, Set dtsFiles) { + private void scanJarForDts(String jarPath, String resourcePath, Set dtsFiles) { try { JarFile jarFile = new JarFile(jarPath); Enumeration entries = jarFile.entries(); - String searchPath = "assets/customnpcs/" + basePath; while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String entryName = entry.getName(); - - // Check if entry is under our base path and is a .d.ts file - if (entryName.startsWith(searchPath) && entryName.endsWith(".d.ts")) { - // Convert to relative path from assets/customnpcs/ - String relativePath = entryName.substring("assets/customnpcs/".length()); + + // Check if entry is under our resource path and is a .d.ts file + if (entryName.startsWith(resourcePath + "/") && entryName.endsWith(".d.ts")) { + // Convert to relative path from resourcePath + String relativePath = entryName.substring(resourcePath.length() + 1); dtsFiles.add(relativePath); } } From 150ffbc8965ffe68e95a8c38f886a8fdb241ddd8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 01:11:46 +0200 Subject: [PATCH 203/337] Improved resolveInheritance logic --- .../interpreter/js_parser/JSTypeRegistry.java | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) 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 db985e9fe..77888054e 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 @@ -356,27 +356,20 @@ public String getHookParameterType(String functionName) { /** * Resolve inheritance relationships between types. + * For each type, walks up the parent chain and resolves all ancestors. + * Efficient O(n) approach - each type's chain is walked once. */ - private void resolveInheritance() { - int totalInheritance = 0; - int resolvedInheritance = 0; - + public void resolveInheritance() { for (JSTypeInfo type : types.values()) { - String extendsType = type.getExtendsType(); - if (extendsType != null) { - totalInheritance++; - JSTypeInfo parent = getType(extendsType); + JSTypeInfo child = type; + while (child != null && child.getExtendsType() != null && child.getResolvedParent() == null) { + JSTypeInfo parent = getType(child.getExtendsType()); if (parent != null) { - type.setResolvedParent(parent); - resolvedInheritance++; - System.out.println("[JSTypeRegistry] Resolved " + type.getSimpleName() + " extends " + parent.getSimpleName()); - } else { - System.err.println("[JSTypeRegistry] FAILED to resolve parent type '" + extendsType + "' for " + type.getFullName()); + child.setResolvedParent(parent); } + child = parent; } } - - System.out.println("[JSTypeRegistry] Inheritance resolution: " + resolvedInheritance + "/" + totalInheritance + " successful"); } /** From 92a196d296fff89b69fb0978074dd9cc06c48910 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 01:12:17 +0200 Subject: [PATCH 204/337] Expanded parser scope + added class parsing --- .../js_parser/TypeScriptDefinitionParser.java | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) 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 e898e6806..446f826bf 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 @@ -12,8 +12,13 @@ public class TypeScriptDefinitionParser { // Patterns for parsing .d.ts content + // Updated to handle generic type parameters like: export interface IEntityLivingBase extends IEntity { private static final Pattern INTERFACE_PATTERN = Pattern.compile( - "export\\s+interface\\s+(\\w+)(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + "export\\s+interface\\s+(\\w+)(?:<[^>]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + + // Similar pattern for classes + private static final Pattern CLASS_PATTERN = Pattern.compile( + "export\\s+class\\s+(\\w+)(?:<[^>]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); private static final Pattern NAMESPACE_PATTERN = Pattern.compile( "export\\s+namespace\\s+(\\w+)\\s*\\{"); @@ -148,7 +153,7 @@ private void parseIndexFile(String content) { } /** - * Parse interface definitions from content. + * Parse interface and class definitions from content. */ private void parseInterfaceFile(String content, String parentNamespace) { // Find interfaces @@ -168,6 +173,8 @@ private void parseInterfaceFile(String content, String parentNamespace) { if (extendsType.contains(",")) { extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim(); } + // Clean up import() syntax if present + extendsType = cleanType(extendsType); typeInfo.setExtends(extendsType); } @@ -182,6 +189,34 @@ private void parseInterfaceFile(String content, String parentNamespace) { registry.registerType(typeInfo); } + // Find classes (same logic as interfaces) + Matcher classMatcher = CLASS_PATTERN.matcher(content); + while (classMatcher.find()) { + String className = classMatcher.group(1); + String extendsClause = classMatcher.group(2); + + JSTypeInfo typeInfo = new JSTypeInfo(className, parentNamespace); + if (extendsClause != null) { + String extendsType = extendsClause.trim(); + extendsType = extendsType.replaceAll("<[^>]*>", ""); + if (extendsType.contains(",")) { + extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim(); + } + extendsType = cleanType(extendsType); + typeInfo.setExtends(extendsType); + } + + // Find the body of this class + int bodyStart = classMatcher.end(); + int bodyEnd = findMatchingBrace(content, bodyStart - 1); + if (bodyEnd > bodyStart) { + String body = content.substring(bodyStart, bodyEnd); + parseInterfaceBody(body, typeInfo); + } + + registry.registerType(typeInfo); + } + // Find namespaces (which contain inner types) Matcher namespaceMatcher = NAMESPACE_PATTERN.matcher(content); while (namespaceMatcher.find()) { From 9c222b89d4894eb0279b8625b912af1797500a4c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 01:36:36 +0200 Subject: [PATCH 205/337] proper JS type names in HoverInfo/Autocomplete instead of "InitEvent", now its "INpcEvent.InitEvent" --- .../script/autocomplete/AutocompleteItem.java | 32 +++++++++++-------- .../interpreter/hover/TokenHoverInfo.java | 32 +++++++++++-------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 80ad10f0c..9d39af289 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -86,8 +86,8 @@ public static AutocompleteItem fromMethod(MethodInfo method) { } insertText.append(")"); - String returnType = method.getReturnType() != null ? - method.getReturnType().getSimpleName() : "void"; + String returnType = method.getReturnType() != null ? + getName(method.getReturnType()) : "void"; String signature = buildMethodSignature(method); @@ -97,8 +97,8 @@ public static AutocompleteItem fromMethod(MethodInfo method) { for (int i = 0; i < method.getParameterCount(); i++) { if (i > 0) displayName.append(", "); FieldInfo param = method.getParameters().get(i); - String paramType = param.getTypeInfo() != null ? - param.getTypeInfo().getSimpleName() : "?"; + String paramType = param.getTypeInfo() != null ? + getName(param.getTypeInfo()) : "?"; displayName.append(paramType); if (param.getName() != null && !param.getName().isEmpty()) { displayName.append(" ").append(param.getName()); @@ -125,8 +125,8 @@ public static AutocompleteItem fromMethod(MethodInfo method) { * Create from a Java FieldInfo. */ public static AutocompleteItem fromField(FieldInfo field) { - String typeLabel = field.getTypeInfo() != null ? - field.getTypeInfo().getSimpleName() : "?"; + String typeLabel = field.getTypeInfo() != null ? + getName(field.getTypeInfo()) : "?"; Kind kind; switch (field.getScope()) { @@ -173,11 +173,11 @@ public static AutocompleteItem fromType(TypeInfo type) { default: kind = Kind.CLASS; } - + String name = getName(type); return new AutocompleteItem( - type.getSimpleName(), - type.getSimpleName(), // searchName same as display name for types - type.getSimpleName(), + name, + name, // searchName same as display name for types + name, kind, type.getPackageName(), type.getFullName(), @@ -259,15 +259,15 @@ public static AutocompleteItem keyword(String keyword) { private static String buildMethodSignature(MethodInfo method) { StringBuilder sb = new StringBuilder(); - String returnType = method.getReturnType() != null ? - method.getReturnType().getSimpleName() : "void"; + String returnType = method.getReturnType() != null ? + getName(method.getReturnType()) : "void"; sb.append(returnType).append(" ").append(method.getName()).append("("); for (int i = 0; i < method.getParameterCount(); i++) { if (i > 0) sb.append(", "); FieldInfo param = method.getParameters().get(i); - String paramType = param.getTypeInfo() != null ? - param.getTypeInfo().getSimpleName() : "?"; + String paramType = param.getTypeInfo() != null ? + getName(param.getTypeInfo()) : "?"; sb.append(paramType).append(" ").append(param.getName()); } @@ -396,6 +396,10 @@ public void addScoreBoost(int boost) { public int getMatchScore() { return matchScore; } public int[] getMatchIndices() { return matchIndices; } + public static String getName(TypeInfo type) { + return type.isJSType() ? type.getFullName() : type.getSimpleName(); + } + public int getParameterCount() { if (sourceData instanceof MethodInfo) { return ((MethodInfo) sourceData).getParameterCount(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 1fde43f32..70617b355 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -509,7 +509,7 @@ private void extractClassInfo(Token token) { int classColor = clazz.isInterface() ? TokenType.INTERFACE_DECL.getHexColor() : clazz.isEnum() ? TokenType.ENUM_DECL.getHexColor() : TokenType.IMPORTED_CLASS.getHexColor(); - addSegment(typeInfo.getSimpleName(), classColor); + addSegment(getName(typeInfo), classColor); // Extends Class superclass = clazz.getSuperclass(); @@ -568,7 +568,7 @@ private void extractClassInfo(Token token) { int classColor = scriptType.getKind() == TypeInfo.Kind.INTERFACE ? TokenType.INTERFACE_DECL.getHexColor() : scriptType.getKind() == TypeInfo.Kind.ENUM ? TokenType.ENUM_DECL.getHexColor() : TokenType.IMPORTED_CLASS.getHexColor(); - addSegment(typeInfo.getSimpleName(), classColor); + addSegment(getName(typeInfo), classColor); // Extends clause for ScriptTypeInfo if (scriptType.hasSuperClass()) { @@ -576,7 +576,7 @@ private void extractClassInfo(Token token) { TypeInfo superClass = scriptType.getSuperClass(); if (superClass != null && superClass.isResolved()) { // Color based on resolved type - addSegment(superClass.getSimpleName(), getColorForTypeInfo(superClass)); + addSegment(getName(superClass), getColorForTypeInfo(superClass)); } else { // Unresolved - show the raw name in undefined color String superName = scriptType.getSuperClassName(); @@ -603,7 +603,7 @@ private void extractClassInfo(Token token) { if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); TypeInfo ifaceType = implementedInterfaces.get(i); - String ifaceName = (i < interfaceNames.size()) ? interfaceNames.get(i) : ifaceType.getSimpleName(); + String ifaceName = (i < interfaceNames.size()) ? interfaceNames.get(i) : getName(ifaceType); if (ifaceType != null && ifaceType.isResolved()) { addSegment(ifaceName, getColorForTypeInfo(ifaceType)); @@ -632,9 +632,9 @@ private void extractClassInfo(Token token) { } else { // Unresolved type iconIndicator = "?"; - addSegment(typeInfo.getSimpleName(), TokenType.IMPORTED_CLASS.getHexColor()); + addSegment(getName(typeInfo), TokenType.IMPORTED_CLASS.getHexColor()); if (!typeInfo.isResolved()) { - errors.add("Cannot resolve class '" + typeInfo.getSimpleName() + "'"); + errors.add("Cannot resolve class '" + getName(typeInfo) + "'"); } } @@ -759,7 +759,7 @@ private void extractGlobalFieldInfo(Token token) { // Type - check for actual type color int typeColor = getColorForTypeInfo(declaredType); - addSegment(declaredType.getSimpleName(), typeColor); + addSegment(getName(declaredType), typeColor); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -800,7 +800,7 @@ private void extractEnumConstantInfo(Token token) { // Type (enum type name) int typeColor = getColorForTypeInfo(enumType); - addSegment(enumType.getSimpleName(), typeColor); + addSegment(getName(enumType), typeColor); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -828,7 +828,7 @@ private void extractLocalFieldInfo(Token token) { TypeInfo declaredType = fieldInfo.getDeclaredType(); if (declaredType != null) { int typeColor = getColorForTypeInfo(declaredType); - addSegment(declaredType.getSimpleName(), typeColor); + addSegment(getName(declaredType), typeColor); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -850,7 +850,7 @@ public String getPackageName(TypeInfo type) { return fullName; } else { String pkg = type.getPackageName(); - String className = type.getSimpleName(); + String className = getName(type); if (pkg != null && !pkg.isEmpty()) { return pkg + "." + className; } else { @@ -876,7 +876,7 @@ private void extractParameterInfo(Token token) { TypeInfo declaredType = fieldInfo.getDeclaredType(); if (declaredType != null) { int typeColor = getColorForTypeInfo(declaredType); - addSegment(declaredType.getSimpleName(), typeColor); + addSegment(getName(declaredType), typeColor); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -889,6 +889,10 @@ private void extractUndefinedInfo(Token token) { iconIndicator = "?"; //addSegment(token.getText(), TokenType.UNDEFINED_VAR.getHexColor()); } + + public String getName(TypeInfo type){ + return type.isJSType()? type.getFullName() : type.getSimpleName(); + } private void extractFieldInfoGeneric(Token token) { FieldInfo fieldInfo = token.getFieldInfo(); @@ -927,7 +931,7 @@ private void buildConstructorDeclaration(MethodInfo constructor, TypeInfo contai TypeInfo paramType = param.getDeclaredType(); if (paramType != null) { int paramTypeColor = getColorForTypeInfo(paramType); - addSegment(paramType.getSimpleName(), paramTypeColor); + addSegment(getName(paramType), paramTypeColor); addSegment(" ", TokenType.DEFAULT.getHexColor()); } addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); @@ -1006,7 +1010,7 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain TypeInfo returnType = methodInfo.getReturnType(); if (returnType != null) { int returnTypeColor = getColorForTypeInfo(returnType); - addSegment(returnType.getSimpleName(), returnTypeColor); + addSegment(getName(returnType), returnTypeColor); addSegment(" ", TokenType.DEFAULT.getHexColor()); } else { addSegment("void ", TokenType.KEYWORD.getHexColor()); @@ -1024,7 +1028,7 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain TypeInfo paramType = param.getDeclaredType(); if (paramType != null) { int paramTypeColor = getColorForTypeInfo(paramType); - addSegment(paramType.getSimpleName(), paramTypeColor); + addSegment(getName(paramType), paramTypeColor); addSegment(" ", TokenType.DEFAULT.getHexColor()); } addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); From 4d78c6bb2e89e55940241d6fad45bd3c3809b087 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 01:48:31 +0200 Subject: [PATCH 206/337] Removed duplicate return type for JS method AutocompleteItem --- .../util/script/autocomplete/AutocompleteItem.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 9d39af289..18b18c3ca 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -197,10 +197,14 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { StringBuilder insertText = new StringBuilder(name); insertText.append("("); insertText.append(")"); - - // Use signature for display name if available, otherwise just name - String displayName = method.getSignature() != null && method.getSignature().contains("(") ? - method.getSignature().substring(method.getSignature().indexOf(name)) : name + "()"; + + // Extract display name from signature: remove return type prefix and keep only name(params) + String displayName = name; + String sig = method.getSignature(); + int returnIndex = sig.lastIndexOf(":"); + if (returnIndex != -1) //remove ":ReturnType" + displayName = sig.substring(0, returnIndex); + return new AutocompleteItem( displayName, From 87931e6c34a80e136fd694ebed38488020525f56 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 02:04:58 +0200 Subject: [PATCH 207/337] Prioritized JS members by inheritance depth (child class members first) --- .../script/autocomplete/AutocompleteItem.java | 52 ++++++++++++++++--- .../autocomplete/JSAutocompleteProvider.java | 26 +++++++--- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 18b18c3ca..b845aae21 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -50,13 +50,16 @@ public int getPriority() { private final boolean requiresImport; // Whether selecting this item requires adding an import private final String importPath; // Full path for import (e.g., "net.minecraft.client.Minecraft") + // Inheritance tracking (for JS types) + private final int inheritanceDepth; // Depth in inheritance tree (0 = child, 1 = parent, 2 = grandparent, etc.) + // Match scoring private int matchScore = 0; // How well this matches the query private int[] matchIndices; // Indices of matched characters for highlighting private AutocompleteItem(String name, String searchName, String insertText, Kind kind, String typeLabel, String signature, String documentation, Object sourceData, boolean deprecated, - boolean requiresImport, String importPath) { + boolean requiresImport, String importPath, int inheritanceDepth) { this.name = name; this.searchName = searchName != null ? searchName : name; // Default to name if not provided this.insertText = insertText; @@ -68,6 +71,7 @@ private AutocompleteItem(String name, String searchName, String insertText, Kind this.deprecated = deprecated; this.requiresImport = requiresImport; this.importPath = importPath; + this.inheritanceDepth = inheritanceDepth; } // ==================== FACTORY METHODS ==================== @@ -117,7 +121,8 @@ public static AutocompleteItem fromMethod(MethodInfo method) { method, false, // TODO: Check for @Deprecated annotation false, // Methods don't require imports - null + null, + -1 // Java methods don't use inheritance depth sorting ); } @@ -154,7 +159,8 @@ public static AutocompleteItem fromField(FieldInfo field) { field, false, false, // Fields don't require imports - null + null, + -1// Java fields don't use inheritance depth sorting ); } @@ -185,7 +191,8 @@ public static AutocompleteItem fromType(TypeInfo type) { type, false, false, // Will be overridden for unimported types - null + null, + -1 // Java types don't use inheritance depth sorting ); } @@ -193,6 +200,15 @@ public static AutocompleteItem fromType(TypeInfo type) { * Create from a JavaScript JSMethodInfo. */ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { + return fromJSMethod(method, 0); + } + + /** + * Create from a JavaScript JSMethodInfo with inheritance depth. + * @param method The method info + * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) + */ + public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritanceDepth) { String name = method.getName(); StringBuilder insertText = new StringBuilder(name); insertText.append("("); @@ -217,7 +233,8 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { method, false, false, - null + null, + inheritanceDepth ); } @@ -225,6 +242,15 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { * Create from a JavaScript JSFieldInfo. */ public static AutocompleteItem fromJSField(JSFieldInfo field) { + return fromJSField(field, 0); + } + + /** + * Create from a JavaScript JSFieldInfo with inheritance depth. + * @param field The field info + * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) + */ + public static AutocompleteItem fromJSField(JSFieldInfo field, int inheritanceDepth) { return new AutocompleteItem( field.getName(), field.getName(), // searchName same as display name @@ -236,7 +262,8 @@ public static AutocompleteItem fromJSField(JSFieldInfo field) { field, false, false, - null + null, + inheritanceDepth ); } @@ -255,7 +282,8 @@ public static AutocompleteItem keyword(String keyword) { null, false, false, - null + null, + -1 // Keywords don't use inheritance depth sorting ); } @@ -526,6 +554,14 @@ public int compareTo(AutocompleteItem other) { return this.kind.getPriority() - other.kind.getPriority(); } + // For JS types, prioritize by inheritance depth (child class members first) + // Only apply if both items have valid inheritance depth (>= 0) + if (this.inheritanceDepth >= 0 && other.inheritanceDepth >= 0) { + if (this.inheritanceDepth != other.inheritanceDepth) { + return this.inheritanceDepth - other.inheritanceDepth; // Lower depth = closer to child = higher priority + } + } + // Finally alphabetically return this.name.compareToIgnoreCase(other.name); } @@ -613,7 +649,7 @@ public AutocompleteItem build() { insertText = name; } return new AutocompleteItem(name, searchName, insertText, kind, typeLabel, - signature, documentation, sourceData, deprecated, requiresImport, importPath); + signature, documentation, sourceData, deprecated, requiresImport, importPath, -1); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index a068bfaf0..e9217a4d3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -101,6 +101,13 @@ protected void addMemberSuggestions(Context context, List item * Recursively add methods from a type and its parents. */ protected void addMethodsFromType(JSTypeInfo type, List items, Set added) { + addMethodsFromType(type, items, added, 0); + } + + /** + * Recursively add methods from a type and its parents with inheritance depth tracking. + */ + private void addMethodsFromType(JSTypeInfo type, List items, Set added, int depth) { for (JSMethodInfo method : type.getMethods().values()) { String name = method.getName(); // Skip overload markers (name$1, name$2, etc.) @@ -109,13 +116,13 @@ protected void addMethodsFromType(JSTypeInfo type, List items, } if (!added.contains(name)) { added.add(name); - items.add(AutocompleteItem.fromJSMethod(method)); + items.add(AutocompleteItem.fromJSMethod(method, depth)); } } - // Add from parent type + // Add from parent type with incremented depth if (type.getResolvedParent() != null) { - addMethodsFromType(type.getResolvedParent(), items, added); + addMethodsFromType(type.getResolvedParent(), items, added, depth + 1); } } @@ -123,16 +130,23 @@ protected void addMethodsFromType(JSTypeInfo type, List items, * Recursively add fields from a type and its parents. */ protected void addFieldsFromType(JSTypeInfo type, List items, Set added) { + addFieldsFromType(type, items, added, 0); + } + + /** + * Recursively add fields from a type and its parents with inheritance depth tracking. + */ + private void addFieldsFromType(JSTypeInfo type, List items, Set added, int depth) { for (JSFieldInfo field : type.getFields().values()) { if (!added.contains(field.getName())) { added.add(field.getName()); - items.add(AutocompleteItem.fromJSField(field)); + items.add(AutocompleteItem.fromJSField(field, depth)); } } - // Add from parent type + // Add from parent type with incremented depth if (type.getResolvedParent() != null) { - addFieldsFromType(type.getResolvedParent(), items, added); + addFieldsFromType(type.getResolvedParent(), items, added, depth + 1); } } From ad5528c25a542d57a35abadc95ed3adf03dfc7f0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 02:14:36 +0200 Subject: [PATCH 208/337] Skipped method calls in method declarations --- .../gui/util/script/interpreter/ScriptDocument.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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 51f83e190..546edb936 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 @@ -2351,6 +2351,15 @@ private void markMethodCalls(List marks) { if (isInImportOrPackage(nameStart)) continue; + boolean skip = false; + for (MethodInfo decl : methods) + if (decl.getNameOffset() == nameStart) + // This is a method declaration, not a call + skip = true; + + if (skip) + continue; + // Find the opening parenthesis by scanning forward from the method name end // The regex includes \( but we scan manually to be safe int openParen = nameEnd; From 662d874f2f6559c957103ceeca02d67c8c47a91b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 02:47:55 +0200 Subject: [PATCH 209/337] Added documentation for JS fields/functions --- .../gui/util/script/interpreter/ScriptDocument.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) 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 546edb936..2b8222cb2 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 @@ -681,8 +681,10 @@ private void parseMethodDeclarations() { // For JS hooks, infer parameter types from registry List params = new ArrayList<>(); TypeInfo returnType = TypeInfo.fromPrimitive("void"); - String documentation = null; - + // Extract documentation before this method + String documentation = extractDocumentationBefore(m.start()); + + if (typeResolver.isJSHook(funcName)) { List sigs = typeResolver.getJSHookSignatures(funcName); if (!sigs.isEmpty()) { @@ -1301,8 +1303,9 @@ private void parseGlobalFields() { initStart = m.start(2); initEnd = m.end(2); } - - FieldInfo fieldInfo = FieldInfo.globalField(varName, typeInfo, position, null, initStart, initEnd, 0); + + String documentation = extractDocumentationBefore(m.start()); + FieldInfo fieldInfo = FieldInfo.globalField(varName, typeInfo, position, documentation, initStart, initEnd, 0); if (globalFields.containsKey(varName)) { AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( From b6d0d77445d0579a937480c67dcd9384f1590712 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 02:55:56 +0200 Subject: [PATCH 210/337] Fixed = sign missing in JS var declaration assignment --- .../script/interpreter/ScriptDocument.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 2b8222cb2..168702fca 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 @@ -975,7 +975,7 @@ private void parseLocalVariables() { if (isJavaScript()) { // JavaScript: var/let/const varName = expr; - Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;\\n]+))?"); + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*(=)\\s*([^;\\n]+))?"); Matcher m = varPattern.matcher(bodyText); while (m.find()) { @@ -983,21 +983,22 @@ private void parseLocalVariables() { if (isExcluded(absPos)) continue; String varName = m.group(1); - String initializer = m.group(2); + String initializer = m.group(3); // Infer type from initializer using resolveExpressionType TypeInfo typeInfo = null; if (initializer != null && !initializer.trim().isEmpty()) { - typeInfo = resolveExpressionType(initializer.trim(), bodyStart + m.start(2)); + typeInfo = resolveExpressionType(initializer.trim(), bodyStart + m.start(3)); } if (typeInfo == null) { typeInfo = TypeInfo.fromClass(Object.class); // Default to Object for unresolved } int initStart = -1, initEnd = -1; - if (initializer != null) { + if (m.group(2) != null) { + // Include the = sign in initStart initStart = bodyStart + m.start(2); - initEnd = bodyStart + m.end(2); + initEnd = bodyStart + m.end(3); } FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, absPos, method, initStart, initEnd, 0); @@ -1269,7 +1270,7 @@ private TypeInfo inferChainType(String firstIdent, int identStart, int dotPositi private void parseGlobalFields() { if (isJavaScript()) { // JavaScript: var/let/const varName = expr; (outside functions) - Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;\\n]+))?"); + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*(=)\\s*([^;\\n]+))?"); Matcher m = varPattern.matcher(text); while (m.find()) { @@ -1287,21 +1288,22 @@ private void parseGlobalFields() { if (insideMethod) continue; String varName = m.group(1); - String initializer = m.group(2); + String initializer = m.group(3); // Infer type from initializer TypeInfo typeInfo = null; if (initializer != null && !initializer.trim().isEmpty()) { - typeInfo = resolveExpressionType(initializer.trim(), m.start(2)); + typeInfo = resolveExpressionType(initializer.trim(), m.start(3)); } if (typeInfo == null) { typeInfo = TypeInfo.fromClass(Object.class); } int initStart = -1, initEnd = -1; - if (initializer != null) { + if (m.group(2) != null) { + // Include the = sign in initStart initStart = m.start(2); - initEnd = m.end(2); + initEnd = m.end(3); } String documentation = extractDocumentationBefore(m.start()); From 6bc932719fb4ca5fcda8cebed4620127cab1c480 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 02:57:12 +0200 Subject: [PATCH 211/337] Checked duplicate methods for JS --- .../client/gui/util/script/interpreter/ScriptDocument.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 168702fca..d6efd73f7 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 @@ -795,9 +795,10 @@ private void parseMethodDeclarations() { methodInfo.validate(methodBodyText, hasBody, (expr, pos) -> resolveExpressionType(expr, pos)); } - // Check for duplicate method declarations - checkDuplicateMethods(); + } + // Check for duplicate method declarations + checkDuplicateMethods(); } /** From 565d52729de5b5dc0bf2ffcd62141cb49f19ff22 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 03:09:22 +0200 Subject: [PATCH 212/337] Added "any" type for JS which is universally compatible with any type, defaulted var declaration type to it --- .../client/gui/util/script/interpreter/ScriptDocument.java | 6 ++++-- .../gui/util/script/interpreter/token/TokenType.java | 3 +++ .../gui/util/script/interpreter/type/TypeChecker.java | 5 +++++ .../client/gui/util/script/interpreter/type/TypeInfo.java | 7 +++++++ 4 files changed, 19 insertions(+), 2 deletions(-) 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 d6efd73f7..04777d514 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 @@ -992,7 +992,8 @@ private void parseLocalVariables() { typeInfo = resolveExpressionType(initializer.trim(), bodyStart + m.start(3)); } if (typeInfo == null) { - typeInfo = TypeInfo.fromClass(Object.class); // Default to Object for unresolved + // Use "any" type for uninitialized variables in JavaScript + typeInfo = TypeInfo.ANY; } int initStart = -1, initEnd = -1; @@ -1297,7 +1298,8 @@ private void parseGlobalFields() { typeInfo = resolveExpressionType(initializer.trim(), m.start(3)); } if (typeInfo == null) { - typeInfo = TypeInfo.fromClass(Object.class); + // Use "any" type for uninitialized variables in JavaScript + typeInfo = TypeInfo.ANY; } int initStart = -1, initEnd = -1; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index 8ee1d0f29..c932fbfec 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -80,6 +80,9 @@ public boolean isItalic() { public static TokenType getByType(TypeInfo typeInfo) { if (typeInfo == null || !typeInfo.isResolved()) return TokenType.UNDEFINED_VAR; + + if ("any".equals(typeInfo.getFullName())) + return TokenType.KEYWORD; // Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly return typeInfo.getTokenType(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index ed3a573fc..e9164f20f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -18,6 +18,11 @@ public static boolean isTypeCompatible(TypeInfo expected, TypeInfo actual) { if (expected == null) return true; // void can accept anything (shouldn't happen) if (actual == null) return true; // Can't verify, assume compatible + // Handle "any" type - universally compatible (JavaScript/TypeScript) + if ("any".equals(expected.getFullName()) || "any".equals(actual.getFullName())) { + return true; + } + // Handle null literal - null is compatible with any reference type (non-primitive) if ("".equals(actual.getFullName())) { Class expectedClass = expected.getJavaClass(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index cf4dbfd11..28f307187 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -36,6 +36,13 @@ public enum Kind { * Singleton constant for the void type. */ public static final TypeInfo VOID = fromPrimitive("void"); + + /** + * Singleton constant for the "any" type (used in JavaScript/TypeScript). + * The "any" type is universally compatible - it can be assigned to anything + * and anything can be assigned to it. + */ + public static final TypeInfo ANY = new TypeInfo("any", "any", "", Kind.CLASS, null, true, null); private final String simpleName; // e.g., "List", "ColorType" private final String fullName; // e.g., "java.util.List", "kamkeel...IOverlay$ColorType" From 35ca7ca479935a4b246714b08d10526f9762412f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 05:07:50 +0200 Subject: [PATCH 213/337] Completely functional JSDoc parsing/stub auto generation for functions and field types, parameters and their return types, and function return types --- .../client/gui/util/GuiScriptTextArea.java | 99 +++++++- .../script/interpreter/ScriptDocument.java | 231 ++++++++++++++++-- .../script/interpreter/jsdoc/JSDocInfo.java | 98 ++++++++ .../interpreter/jsdoc/JSDocParamTag.java | 43 ++++ .../script/interpreter/jsdoc/JSDocParser.java | 217 ++++++++++++++++ .../interpreter/jsdoc/JSDocReturnTag.java | 29 +++ .../script/interpreter/jsdoc/JSDocTag.java | 55 +++++ .../interpreter/jsdoc/JSDocTypeTag.java | 27 ++ .../script/interpreter/token/TokenType.java | 13 +- 9 files changed, 784 insertions(+), 28 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParamTag.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocReturnTag.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTag.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java 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 2950b08f9..e14995162 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1615,11 +1615,12 @@ private boolean handleInsertionKeys(int i) { } } - // Generate properly indented javadoc block - String javadocStub = "\n" + indent + " * \n" + indent + " */"; + // Look ahead for a function declaration to generate smart JSDoc + String after = getSelectionAfterText(); + String javadocStub = generateJSDocStub(after, indent); addText(javadocStub); - // Position cursor after " * " on the middle line + // Position cursor after " * " on the description line int newCursorPos = before.length() + 1 + indent.length() + 3; // +1 for \n, +3 for " * " selection.reset(newCursorPos); scrollToCursor(); @@ -1702,6 +1703,98 @@ private boolean handleInsertionKeys(int i) { return false; } + + /** + * Generates a JSDoc stub for a function declaration. + * Looks at the text after the cursor to find function signature and generates + * appropriate @param and @return tags. + * + * @param after Text after the /** to search for function + * @param indent Current line indentation + * @return The JSDoc stub string including newlines + */ + private String generateJSDocStub(String after, String indent) { + StringBuilder stub = new StringBuilder(); + stub.append("\n").append(indent).append(" * "); // Description line + + // Try to find a function/method declaration following the JSDoc + // Patterns: + // Java: [modifiers] ReturnType methodName(params) { + // JS: function funcName(params) { or var funcName = function(params) { or funcName(params) { + + // Skip whitespace and newlines + String trimmed = after.replaceFirst("^[\\s\\n\\r]*", ""); + + // Pattern for Java/JS method/function + java.util.regex.Pattern funcPattern = java.util.regex.Pattern.compile( + // Group 1: Optional return type (Java) or 'function' keyword (JS) + "^(?:(\\w+(?:<[^>]+>)?|function)\\s+)?" + + // Group 2: Method/function name + "(\\w+)\\s*" + + // Group 3: Parameters inside parentheses + "\\(([^)]*)\\)" + ); + + java.util.regex.Matcher m = funcPattern.matcher(trimmed); + if (m.find()) { + String returnOrKeyword = m.group(1); + String funcName = m.group(2); + String paramsStr = m.group(3) != null ? m.group(3).trim() : ""; + + // Determine if this is JavaScript (function keyword or no return type) + boolean isJS = "function".equals(returnOrKeyword) || returnOrKeyword == null || + (container != null && container.getDocument() != null && container.getDocument().isJavaScript()); + + // Parse parameters + if (!paramsStr.isEmpty()) { + String[] params = paramsStr.split(","); + for (String param : params) { + param = param.trim(); + if (param.isEmpty()) continue; + + String paramName; + String paramType = "any"; + + // Split on whitespace to get type and name + String[] parts = param.split("\\s+"); + if (parts.length >= 2) { + // Java style: Type name + paramType = parts[parts.length - 2]; + paramName = parts[parts.length - 1]; + } else { + // JS style: just name + paramName = parts[0]; + } + + // Remove any trailing array brackets or varargs + paramName = paramName.replaceAll("[\\[\\]\\.]", ""); + + if (isJS) { + stub.append("\n").append(indent).append(" * @param {").append(paramType).append("} ").append(paramName); + } else { + stub.append("\n").append(indent).append(" * @param ").append(paramName); + } + } + } + + // Add @return/@returns tag if applicable + if (returnOrKeyword != null && !"function".equals(returnOrKeyword) && !"void".equals(returnOrKeyword)) { + if (isJS) { + stub.append("\n").append(indent).append(" * @returns {").append(returnOrKeyword).append("}"); + } else { + stub.append("\n").append(indent).append(" * @return"); + } + } else if (isJS) { + // For JS functions without explicit return type, add empty @returns + stub.append("\n").append(indent).append(" * @returns {any}"); + } + } + + // Close the JSDoc block + stub.append("\n").append(indent).append(" */"); + + return stub.toString(); + } /** * Handles deletion keys: Delete, Backspace, and Ctrl+Backspace. 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 04777d514..12439e28b 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 @@ -8,23 +8,18 @@ import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSScriptAnalyzer; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParser; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature; import noppes.npcs.client.gui.util.script.interpreter.token.Token; import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; -import noppes.npcs.client.gui.util.script.interpreter.type.ImportData; -import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; -import scala.annotation.meta.field; +import noppes.npcs.client.gui.util.script.interpreter.type.*; import java.util.*; import java.util.regex.Matcher; @@ -121,6 +116,9 @@ public class ScriptDocument { // Type resolver private final TypeResolver typeResolver; + // JSDoc parser for extracting type information from JSDoc comments + private final JSDocParser jsDocParser = new JSDocParser(this); + // Script language: "ECMAScript", "Groovy", etc. private String language = "ECMAScript"; @@ -678,12 +676,15 @@ private void parseMethodDeclarations() { int bodyEnd = findMatchingBrace(bodyStart); if (bodyEnd < 0) bodyEnd = text.length(); + // Extract documentation before this method + String documentation = extractDocumentationBefore(m.start()); + + // Check for JSDoc before the function declaration + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start()); + // For JS hooks, infer parameter types from registry List params = new ArrayList<>(); TypeInfo returnType = TypeInfo.fromPrimitive("void"); - // Extract documentation before this method - String documentation = extractDocumentationBefore(m.start()); - if (typeResolver.isJSHook(funcName)) { List sigs = typeResolver.getJSHookSignatures(funcName); @@ -703,17 +704,32 @@ private void parseMethodDeclarations() { } } } else { - // Non-hook function - params with 'any' type + // Non-hook function - use JSDoc param types if available, else 'any' type if (paramList != null && !paramList.trim().isEmpty()) { int paramOffset = m.start(2); for (String p : paramList.split(",")) { String pn = p.trim(); if (!pn.isEmpty()) { int paramStart = paramOffset + paramList.indexOf(pn); - params.add(FieldInfo.parameter(pn, TypeInfo.fromClass(Object.class), paramStart, null)); + + // Check if JSDoc has a @param tag for this parameter + TypeInfo paramType = TypeInfo.fromClass(Object.class); + if (jsDoc != null) { + JSDocParamTag paramTag = jsDoc.getParamTag(pn); + if (paramTag != null && paramTag.getTypeInfo() != null) { + paramType = paramTag.getTypeInfo(); + } + } + + params.add(FieldInfo.parameter(pn, paramType, paramStart, null)); } } } + + // Use JSDoc @return type if available + if (jsDoc != null && jsDoc.hasReturnTag() && jsDoc.getReturnType() != null) { + returnType = jsDoc.getReturnType(); + } } // Create MethodInfo and add to shared methods list @@ -986,13 +1002,24 @@ private void parseLocalVariables() { String varName = m.group(1); String initializer = m.group(3); - // Infer type from initializer using resolveExpressionType + // Check for JSDoc type annotation before the declaration + int absStart = bodyStart + m.start(); + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, absStart); + TypeInfo typeInfo = null; - if (initializer != null && !initializer.trim().isEmpty()) { + + // Priority 1: JSDoc @type takes precedence + if (jsDoc != null && jsDoc.hasTypeTag()) { + typeInfo = jsDoc.getDeclaredType(); + } + + // Priority 2: Infer type from initializer if no JSDoc type + if (typeInfo == null && initializer != null && !initializer.trim().isEmpty()) { typeInfo = resolveExpressionType(initializer.trim(), bodyStart + m.start(3)); } + + // Priority 3: Use "any" type for uninitialized variables if (typeInfo == null) { - // Use "any" type for uninitialized variables in JavaScript typeInfo = TypeInfo.ANY; } @@ -1292,13 +1319,24 @@ private void parseGlobalFields() { String varName = m.group(1); String initializer = m.group(3); - // Infer type from initializer + // First, check for JSDoc type annotation + String documentation = extractDocumentationBefore(m.start()); + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start()); + TypeInfo typeInfo = null; - if (initializer != null && !initializer.trim().isEmpty()) { + + // Priority 1: JSDoc @type takes precedence + if (jsDoc != null && jsDoc.hasTypeTag()) { + typeInfo = jsDoc.getDeclaredType(); + } + + // Priority 2: Infer from initializer if no JSDoc type + if (typeInfo == null && initializer != null && !initializer.trim().isEmpty()) { typeInfo = resolveExpressionType(initializer.trim(), m.start(3)); } + + // Priority 3: Use "any" type for uninitialized variables if (typeInfo == null) { - // Use "any" type for uninitialized variables in JavaScript typeInfo = TypeInfo.ANY; } @@ -1309,7 +1347,6 @@ private void parseGlobalFields() { initEnd = m.end(3); } - String documentation = extractDocumentationBefore(m.start()); FieldInfo fieldInfo = FieldInfo.globalField(varName, typeInfo, position, documentation, initStart, initEnd, 0); if (globalFields.containsKey(varName)) { @@ -1702,10 +1739,15 @@ private TypeInfo resolveTypeAndTrackUsage(String typeName) { private List buildMarks() { List marks = new ArrayList<>(); - // Comments and strings first (highest priority) - same for both languages - addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT); + // Strings first to protect their content addPatternMarks(marks, STRING_PATTERN, TokenType.STRING); + // JSDoc comments with fragmented marking (avoids conflicts with @tags and {Type}) + markJSDocElements(marks); + + // Regular comments (non-JSDoc) + markNonJSDocComments(marks); + // Keywords - same for both languages (KEYWORD_PATTERN includes JS keywords like function, var, let, const) addPatternMarks(marks, KEYWORD_PATTERN, TokenType.KEYWORD); @@ -2310,6 +2352,149 @@ else if (content.charAt(i) == '>') } } + /** + * Mark JSDoc elements within comments for syntax highlighting. + * Adds marks for @tags (like @param, @return, @type) and {Type} references. + */ + private void markJSDocElements(List marks) { + // Find all JSDoc comments (/** ... */) + Pattern jsDocPattern = Pattern.compile("/\\*\\*([\\s\\S]*?)\\*/"); + Matcher jsDocMatcher = jsDocPattern.matcher(text); + + while (jsDocMatcher.find()) { + int commentStart = jsDocMatcher.start(); + int commentEnd = jsDocMatcher.end(); + + // Skip if this JSDoc is inside a string + boolean insideString = false; + for (ScriptLine.Mark mark : marks) { + if (mark.type == TokenType.STRING && + commentStart >= mark.start && commentEnd <= mark.end) { + insideString = true; + break; + } + } + if (insideString) { + continue; + } + + String commentContent = jsDocMatcher.group(0); + + // Collect all special element positions (@tags and {Type}s) that should NOT be gray + java.util.List specialRanges = new java.util.ArrayList<>(); + + // Find @tags + Pattern tagPattern = Pattern.compile("@(\\w+)"); + Matcher tagMatcher = tagPattern.matcher(commentContent); + while (tagMatcher.find()) { + int tagStart = commentStart + tagMatcher.start(); + int tagEnd = commentStart + tagMatcher.end(); + specialRanges.add(new int[]{tagStart, tagEnd}); + // Mark the @tag itself + marks.add(new ScriptLine.Mark(tagStart, tagEnd, TokenType.JSDOC_TAG, null)); + } + + // Find {Type} references + Pattern typePattern = Pattern.compile("\\{([^}]+)\\}"); + Matcher typeMatcher = typePattern.matcher(commentContent); + while (typeMatcher.find()) { + int braceStart = commentStart + typeMatcher.start(); + int braceEnd = commentStart + typeMatcher.end(); + specialRanges.add(new int[]{braceStart, braceEnd}); + + // Mark braces + marks.add(new ScriptLine.Mark(braceStart, braceStart + 1, TokenType.JSDOC_TYPE, null)); // { + marks.add(new ScriptLine.Mark(braceEnd - 1, braceEnd, TokenType.JSDOC_TYPE, null)); // } + + // Mark the type name inside braces + String typeName = typeMatcher.group(1).trim(); + int typeStart = commentStart + typeMatcher.start(1); + int typeEnd = commentStart + typeMatcher.end(1); + + // Try to resolve the type for hover info + TypeInfo resolvedType = resolveType(typeName); + marks.add(new ScriptLine.Mark(typeStart, typeEnd, resolvedType != null? resolvedType.getTokenType() : TokenType.UNDEFINED_VAR, resolvedType)); + } + + // Sort special ranges by start position + specialRanges.sort((a, b) -> Integer.compare(a[0], b[0])); + + // Mark the comment in FRAGMENTS between special elements + int lastPos = commentStart; + for (int[] range : specialRanges) { + // Mark from lastPos to start of special element + if (lastPos < range[0]) { + marks.add(new ScriptLine.Mark(lastPos, range[0], TokenType.COMMENT, null)); + } + lastPos = range[1]; // Move past the special element + } + // Mark from last special element to end of comment + if (lastPos < commentEnd) { + marks.add(new ScriptLine.Mark(lastPos, commentEnd, TokenType.COMMENT, null)); + } + } + } + + /** + * Mark all comments EXCEPT JSDoc comments (slash-star-star style). + * JSDoc comments are handled separately in markJSDocElements. + */ + private void markNonJSDocComments(List marks) { + // Match: /* ... */ (but not /**), // ..., # ... + Matcher m = COMMENT_PATTERN.matcher(text); + while (m.find()) { + int start = m.start(); + int end = m.end(); + String comment = m.group(); + + // Skip JSDoc comments (/** style) - they're handled separately + if (comment.startsWith("/**")) { + continue; + } + + marks.add(new ScriptLine.Mark(start, end, TokenType.COMMENT, null)); + } + } + + /** + * Resolve a type name from JSDoc to a TypeInfo. + */ + private TypeInfo resolveJSDocType(String typeName) { + if (typeName == null || typeName.isEmpty()) { + return null; + } + + // Handle union types - use first type + if (typeName.contains("|")) { + typeName = typeName.split("\\|")[0].trim(); + } + + // Handle nullable + typeName = typeName.replace("?", "").trim(); + + // Map JSDoc types to Java types + switch (typeName.toLowerCase()) { + case "string": + return TypeInfo.fromClass(String.class); + case "number": + case "int": + case "integer": + return isJavaScript() ? TypeInfo.fromClass(double.class) : TypeInfo.fromClass(int.class); + case "boolean": + case "bool": + return TypeInfo.fromClass(boolean.class); + case "void": + return TypeInfo.fromPrimitive("void"); + case "any": + case "object": + case "*": + return TypeInfo.ANY; + } + + // Try through the type resolver + return resolveType(typeName); + } + private void markMethodDeclarations(List marks) { Matcher m = METHOD_DECL_PATTERN.matcher(text); while (m.find()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java new file mode 100644 index 000000000..1cd0050b4 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java @@ -0,0 +1,98 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Represents parsed JSDoc comment information. + * Captures @type, @param, @return, and other JSDoc tags with their type information. + */ +public class JSDocInfo { + + private final String rawComment; + private final int startOffset; + private final int endOffset; + + private JSDocTypeTag typeTag; + private final List paramTags = new ArrayList<>(); + private JSDocReturnTag returnTag; + private String description; + private final List allTags = new ArrayList<>(); + + public JSDocInfo(String rawComment, int startOffset, int endOffset) { + this.rawComment = rawComment; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + public void setTypeTag(JSDocTypeTag typeTag) { + this.typeTag = typeTag; + allTags.add(typeTag); + } + + public void addParamTag(JSDocParamTag paramTag) { + this.paramTags.add(paramTag); + allTags.add(paramTag); + } + + public void setReturnTag(JSDocReturnTag returnTag) { + this.returnTag = returnTag; + allTags.add(returnTag); + } + + public void setDescription(String description) { + this.description = description; + } + + public void addTag(JSDocTag tag) { + allTags.add(tag); + } + + public String getRawComment() { return rawComment; } + public int getStartOffset() { return startOffset; } + public int getEndOffset() { return endOffset; } + public JSDocTypeTag getTypeTag() { return typeTag; } + public boolean hasTypeTag() { return typeTag != null; } + + public TypeInfo getDeclaredType() { + return typeTag != null ? typeTag.getTypeInfo() : null; + } + + public List getParamTags() { + return Collections.unmodifiableList(paramTags); + } + + public boolean hasParamTags() { return !paramTags.isEmpty(); } + + public JSDocParamTag getParamTag(String paramName) { + for (JSDocParamTag tag : paramTags) { + if (tag.getParamName().equals(paramName)) { + return tag; + } + } + return null; + } + + public JSDocReturnTag getReturnTag() { return returnTag; } + public boolean hasReturnTag() { return returnTag != null; } + + public TypeInfo getReturnType() { + return returnTag != null ? returnTag.getTypeInfo() : null; + } + + public String getDescription() { return description; } + public List getAllTags() { return Collections.unmodifiableList(allTags); } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("JSDocInfo{"); + if (typeTag != null) sb.append("type=").append(typeTag.getTypeName()); + if (!paramTags.isEmpty()) sb.append(", params=").append(paramTags.size()); + if (returnTag != null) sb.append(", return=").append(returnTag.getTypeName()); + sb.append("}"); + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParamTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParamTag.java new file mode 100644 index 000000000..cdabdb717 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParamTag.java @@ -0,0 +1,43 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents an @param JSDoc tag. + * Used to declare the type and description of a function parameter. + * Example: @param {string} name - The name of the person + */ +public class JSDocParamTag extends JSDocTag { + + private final String paramName; + private final int paramNameStart; + private final int paramNameEnd; + + public JSDocParamTag(int atSignOffset, int tagNameStart, int tagNameEnd, + String paramName, int paramNameStart, int paramNameEnd) { + super("param", atSignOffset, tagNameStart, tagNameEnd); + this.paramName = paramName; + this.paramNameStart = paramNameStart; + this.paramNameEnd = paramNameEnd; + } + + public static JSDocParamTag create(int atSignOffset, int tagNameStart, int tagNameEnd, + String typeName, TypeInfo typeInfo, int typeStart, int typeEnd, + String paramName, int paramNameStart, int paramNameEnd, + String description) { + JSDocParamTag tag = new JSDocParamTag(atSignOffset, tagNameStart, tagNameEnd, + paramName, paramNameStart, paramNameEnd); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + tag.setDescription(description); + return tag; + } + + public String getParamName() { return paramName; } + public int getParamNameStart() { return paramNameStart; } + public int getParamNameEnd() { return paramNameEnd; } + + @Override + public String toString() { + return "JSDocParamTag{@param " + (typeName != null ? "{" + typeName + "} " : "") + paramName + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java new file mode 100644 index 000000000..e93ed12eb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java @@ -0,0 +1,217 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses JSDoc comments to extract type information. + * Handles @type, @param, @return/@returns tags. + */ +public class JSDocParser { + + private static final Pattern JSDOC_PATTERN = Pattern.compile( + "/\\*\\*([\\s\\S]*?)\\*/", Pattern.MULTILINE); + + private static final Pattern TYPE_TAG_PATTERN = Pattern.compile( + "@(type)\\s*\\{([^}]+)\\}", Pattern.CASE_INSENSITIVE); + + private static final Pattern PARAM_TAG_PATTERN = Pattern.compile( + "@(param)\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)(?:\\s*-?\\s*(.*))?", Pattern.CASE_INSENSITIVE); + + private static final Pattern RETURN_TAG_PATTERN = Pattern.compile( + "@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?", Pattern.CASE_INSENSITIVE); + + private static final Pattern ANY_TAG_PATTERN = Pattern.compile( + "@(\\w+)", Pattern.CASE_INSENSITIVE); + + private final ScriptDocument document; + + public JSDocParser(ScriptDocument typeResolver) { + this.document = typeResolver; + } + + public JSDocInfo parse(String comment, int commentStart) { + if (comment == null || !comment.startsWith("/**")) { + return null; + } + + int commentEnd = commentStart + comment.length(); + JSDocInfo info = new JSDocInfo(comment, commentStart, commentEnd); + + String content = comment.substring(3); + if (content.endsWith("*/")) { + content = content.substring(0, content.length() - 2); + } + + int contentOffset = commentStart + 3; + + parseTypeTag(content, contentOffset, info); + parseParamTags(content, contentOffset, info); + parseReturnTag(content, contentOffset, info); + extractDescription(content, info); + + return info; + } + + public JSDocInfo extractJSDocBefore(String source, int position) { + if (source == null || position <= 0) { + return null; + } + + int searchStart = Math.max(0, position - 1); + + while (searchStart > 0 && Character.isWhitespace(source.charAt(searchStart))) { + searchStart--; + } + + int endCommentPos = -1; + if (searchStart >= 1 && source.charAt(searchStart) == '/' && source.charAt(searchStart - 1) == '*') { + endCommentPos = searchStart; + } else if (searchStart >= 1 && source.charAt(searchStart - 1) == '/' && searchStart > 1 && source.charAt( + searchStart - 2) == '*') { + endCommentPos = searchStart - 1; + } + + if (endCommentPos < 0) { + return null; + } + + int startCommentPos = -1; + for (int i = endCommentPos - 1; i >= 2; i--) { + if (source.charAt(i) == '*' && source.charAt(i - 1) == '*' && source.charAt(i - 2) == '/') { + startCommentPos = i - 2; + break; + } + } + + if (startCommentPos < 0) { + return null; + } + + String comment = source.substring(startCommentPos, endCommentPos + 1); + return parse(comment, startCommentPos); + } + + private void parseTypeTag(String content, int contentOffset, JSDocInfo info) { + Matcher m = TYPE_TAG_PATTERN.matcher(content); + if (m.find()) { + int atSignOffset = contentOffset + m.start(); + int tagNameStart = contentOffset + m.start(1); + int tagNameEnd = contentOffset + m.end(1); + + String typeName = m.group(2).trim(); + + int braceStart = content.indexOf('{', m.start()); + int braceEnd = content.indexOf('}', braceStart); + int typeStart = contentOffset + braceStart + 1; + int typeEnd = contentOffset + braceEnd; + + TypeInfo typeInfo = resolveType(typeName); + + JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd); + info.setTypeTag(tag); + } + } + + private void parseParamTags(String content, int contentOffset, JSDocInfo info) { + Matcher m = PARAM_TAG_PATTERN.matcher(content); + while (m.find()) { + int atSignOffset = contentOffset + m.start(); + int tagNameStart = contentOffset + m.start(1); + int tagNameEnd = contentOffset + m.end(1); + + String typeName = m.group(2); + String paramName = m.group(3); + String description = m.group(4); + + int typeStart = -1; + int typeEnd = -1; + TypeInfo typeInfo = null; + + if (typeName != null) { + typeName = typeName.trim(); + int braceStart = content.indexOf('{', m.start()); + int braceEnd = content.indexOf('}', braceStart); + typeStart = contentOffset + braceStart + 1; + typeEnd = contentOffset + braceEnd; + typeInfo = resolveType(typeName); + } + + int paramNameStart = contentOffset + m.start(3); + int paramNameEnd = contentOffset + m.end(3); + + JSDocParamTag tag = JSDocParamTag.create(atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd, + paramName, paramNameStart, paramNameEnd, + description != null ? description.trim() : null); + info.addParamTag(tag); + } + } + + private void parseReturnTag(String content, int contentOffset, JSDocInfo info) { + Matcher m = RETURN_TAG_PATTERN.matcher(content); + if (m.find()) { + String tagName = m.group(1); + int atSignOffset = contentOffset + m.start(); + int tagNameStart = contentOffset + m.start(1); + int tagNameEnd = contentOffset + m.end(1); + + String typeName = m.group(2); + String description = m.group(3); + + int typeStart = -1; + int typeEnd = -1; + TypeInfo typeInfo = null; + + if (typeName != null) { + typeName = typeName.trim(); + int braceStart = content.indexOf('{', m.start()); + int braceEnd = content.indexOf('}', braceStart); + typeStart = contentOffset + braceStart + 1; + typeEnd = contentOffset + braceEnd; + typeInfo = resolveType(typeName); + } + + JSDocReturnTag tag = JSDocReturnTag.create(tagName, atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd, + description != null ? description.trim() : null); + info.setReturnTag(tag); + } + } + + private void extractDescription(String content, JSDocInfo info) { + Matcher m = ANY_TAG_PATTERN.matcher(content); + String description; + + if (m.find()) { + description = content.substring(0, m.start()); + } else { + description = content; + } + + description = description.replaceAll("(?m)^\\s*\\*\\s?", "").trim(); + + if (!description.isEmpty()) { + info.setDescription(description); + } + } + + private TypeInfo resolveType(String typeName) { + return document.resolveType(typeName); + } + + public static List findAllJSDocComments(String source) { + List positions = new ArrayList<>(); + Matcher m = JSDOC_PATTERN.matcher(source); + while (m.find()) { + positions.add(new int[]{m.start(), m.end()}); + } + return positions; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocReturnTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocReturnTag.java new file mode 100644 index 000000000..0f5fca416 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocReturnTag.java @@ -0,0 +1,29 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents an @return or @returns JSDoc tag. + * Used to declare the return type of a function. + * Example: @return {number} The calculated result + */ +public class JSDocReturnTag extends JSDocTag { + + public JSDocReturnTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd) { + super(tagName, atSignOffset, tagNameStart, tagNameEnd); + } + + public static JSDocReturnTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, + String typeName, TypeInfo typeInfo, int typeStart, int typeEnd, + String description) { + JSDocReturnTag tag = new JSDocReturnTag(tagName, atSignOffset, tagNameStart, tagNameEnd); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + tag.setDescription(description); + return tag; + } + + @Override + public String toString() { + return "JSDocReturnTag{@" + tagName + " " + (typeName != null ? "{" + typeName + "}" : "") + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTag.java new file mode 100644 index 000000000..82fd1edbe --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTag.java @@ -0,0 +1,55 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Base class for JSDoc tags. + * Each tag stores its position information for syntax highlighting. + */ +public class JSDocTag { + + protected final String tagName; + protected final int atSignOffset; + protected final int tagNameStart; + protected final int tagNameEnd; + + protected String typeName; + protected TypeInfo typeInfo; + protected int typeStart = -1; + protected int typeEnd = -1; + protected String description; + + public JSDocTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd) { + this.tagName = tagName; + this.atSignOffset = atSignOffset; + this.tagNameStart = tagNameStart; + this.tagNameEnd = tagNameEnd; + } + + public void setType(String typeName, TypeInfo typeInfo, int typeStart, int typeEnd) { + this.typeName = typeName; + this.typeInfo = typeInfo; + this.typeStart = typeStart; + this.typeEnd = typeEnd; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getTagName() { return tagName; } + public int getAtSignOffset() { return atSignOffset; } + public int getTagNameStart() { return tagNameStart; } + public int getTagNameEnd() { return tagNameEnd; } + public String getTypeName() { return typeName; } + public TypeInfo getTypeInfo() { return typeInfo; } + public boolean hasType() { return typeName != null && !typeName.isEmpty(); } + public int getTypeStart() { return typeStart; } + public int getTypeEnd() { return typeEnd; } + public String getDescription() { return description; } + + @Override + public String toString() { + return "JSDocTag{@" + tagName + (typeName != null ? " {" + typeName + "}" : "") + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java new file mode 100644 index 000000000..0ec2c1389 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java @@ -0,0 +1,27 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +/** + * Represents an @type JSDoc tag. + * Used to declare the type of a variable. + * Example: @type {string} + */ +public class JSDocTypeTag extends JSDocTag { + + public JSDocTypeTag(int atSignOffset, int tagNameStart, int tagNameEnd) { + super("type", atSignOffset, tagNameStart, tagNameEnd); + } + + public static JSDocTypeTag create(int atSignOffset, int tagNameStart, int tagNameEnd, + String typeName, TypeInfo typeInfo, int typeStart, int typeEnd) { + JSDocTypeTag tag = new JSDocTypeTag(atSignOffset, tagNameStart, tagNameEnd); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + return tag; + } + + @Override + public String toString() { + return "JSDocTypeTag{@type " + (typeName != null ? "{" + typeName + "}" : "") + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java index c932fbfec..2cb57da62 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/token/TokenType.java @@ -9,8 +9,13 @@ */ public enum TokenType { // Comments and strings have highest priority - they override everything inside them - COMMENT(0x777777, 130), - STRING(0xCC8855, 120), + COMMENT(0x777777, 140), + STRING(0xCC8855, 130), + + // JSDoc elements - lower priority but will fill gaps left by fragmented comment marking + JSDOC_TAG(0xCC9933, 125), // @param, @type, @return etc. (gold/orange) + JSDOC_TYPE(0x00AAAA, 124), // {TypeName} in JSDoc (aqua like types) + UNUSED_IMPORT(0x666666, 119), // unused import statements (gray) // Keywords and modifiers @@ -104,6 +109,10 @@ public char toColorCode() { return '7'; // gray case STRING: return '5'; // purple + case JSDOC_TAG: + return '6'; // gold + case JSDOC_TYPE: + return '3'; // dark aqua (like types) case CLASS_KEYWORD: case KEYWORD: return 'c'; // red From 761b59479e4230ae50669b621a46163aca225e31 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 05:08:00 +0200 Subject: [PATCH 214/337] lil tweak --- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 70617b355..12b9099ea 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -632,7 +632,7 @@ private void extractClassInfo(Token token) { } else { // Unresolved type iconIndicator = "?"; - addSegment(getName(typeInfo), TokenType.IMPORTED_CLASS.getHexColor()); + addSegment(getName(typeInfo), token.getType().getHexColor()); if (!typeInfo.isResolved()) { errors.add("Cannot resolve class '" + getName(typeInfo) + "'"); } From cd942fea30f0ba36bea994485047a24f36f29df9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 05:25:39 +0200 Subject: [PATCH 215/337] Inferred type of 'any' vars upon their first assignment i.e.: var npc npc = event.getNpc() npc becomes ICustomNpc --- .../script/interpreter/ScriptDocument.java | 27 +++++++++-- .../interpreter/field/FieldAccessInfo.java | 2 +- .../script/interpreter/field/FieldInfo.java | 48 ++++++++++++++++++- .../interpreter/hover/TokenHoverInfo.java | 10 ++-- .../interpreter/method/MethodCallInfo.java | 2 +- .../script/interpreter/method/MethodInfo.java | 4 +- .../interpreter/type/ScriptTypeInfo.java | 2 +- 7 files changed, 80 insertions(+), 15 deletions(-) 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 12439e28b..5318490de 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 @@ -4480,7 +4480,7 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in // Simple variable targetField = resolveVariable(targetName, lhsStart); if (targetField != null) { - targetType = targetField.getDeclaredType(); + targetType = targetField.getTypeInfo(); reflectionField = targetField.getReflectionField(); } } @@ -4494,6 +4494,16 @@ private void createAndAttachAssignment(String lhs, String rhs, int stmtStart, in ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null; } + // Type inference: If the target field has "any" type and no inferred type yet, + // set the inferred type from the resolved source type (first assignment wins) + if (targetField != null && targetField.canInferType() && targetField.getInferredType() == null + && sourceType != null && sourceType.isResolved()) { + // Don't infer "any" type - that doesn't refine anything + if (!"any".equals(sourceType.getFullName())) { + targetField.setInferredType(sourceType); + } + } + // Determine if this is a script-defined field or external field FieldInfo finalTargetField = targetField; boolean isScriptField = targetField != null && @@ -4590,7 +4600,7 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st return; // Field doesn't exist, can't attach assignment } - TypeInfo targetType = targetField.getDeclaredType(); + TypeInfo targetType = targetField.getTypeInfo(); // Resolve the source type with expected type context TypeInfo sourceType; @@ -4601,6 +4611,15 @@ private void createAndAttachDeclarationAssignment(String lhs, String rhs, int st ExpressionTypeResolver.CURRENT_EXPECTED_TYPE = null; } + // Type inference: If the target field has "any" type and no inferred type yet, + // set the inferred type from the resolved source type + if (targetField.canInferType() && targetField.getInferredType() == null && sourceType != null && sourceType.isResolved()) { + // Don't infer "any" type - that doesn't refine anything + if (!"any".equals(sourceType.getFullName())) { + targetField.setInferredType(sourceType); + } + } + // Create the assignment info // Declaration assignments should NOT check final status - this is the one place where final fields can be assigned AssignmentInfo info = new AssignmentInfo( @@ -5575,14 +5594,14 @@ private TypeInfo lookupVariableType(String varName, int position) { if (locals != null && locals.containsKey(varName)) { FieldInfo field = locals.get(varName); if (field.isVisibleAt(position)) { - return field.getDeclaredType(); + return field.getTypeInfo(); } } } // Check global fields if (globalFields.containsKey(varName)) { - return globalFields.get(varName).getDeclaredType(); + return globalFields.get(varName).getTypeInfo(); } return null; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java index dc29943e3..fc87898f7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldAccessInfo.java @@ -98,7 +98,7 @@ public void validate() { // Check return type compatibility with expected type (e.g., assignment LHS) if (expectedType != null && resolvedField != null) { - TypeInfo fieldType = resolvedField.getDeclaredType(); + TypeInfo fieldType = resolvedField.getTypeInfo(); if (fieldType != null && !TypeChecker.isTypeCompatible(expectedType, fieldType)) { //extra space is necessary for alignment //setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index d10218d83..5d6b6efdd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -57,6 +57,11 @@ public enum Scope { // Enum constant specific info private EnumConstantInfo enumConstantInfo; + // Type inference support (for JavaScript "any" typed variables) + // When a variable is declared without type (var x;), it starts as "any" but can be + // refined through assignments or JSDoc comments + private TypeInfo inferredType; + private FieldInfo(String name, Scope scope, TypeInfo declaredType, int declarationOffset, boolean resolved, MethodInfo containingMethod, String documentation, int initStart, int initEnd, int modifiers, @@ -411,12 +416,53 @@ public java.lang.reflect.Field getReflectionField() { return reflectionField; } + // ==================== TYPE INFERENCE ==================== + + /** + * Get the inferred type for this field. + * This is set when a variable declared as "any" has its type refined through + * assignment analysis or JSDoc comments. + */ + public TypeInfo getInferredType() { + return inferredType; + } + + /** + * Set the inferred type for this field. + * Used when type inference determines a more specific type than the declared type. + * @param inferredType The refined type based on assignments or JSDoc + */ + public void setInferredType(TypeInfo inferredType) { + this.inferredType = inferredType; + } + + /** + * Check if this field has a type that can be refined (currently "any" type). + */ + public boolean canInferType() { + return declaredType != null && "any".equals(declaredType.getFullName()); + } + + /** + * Get the effective type for this field, considering inference. + * Returns inferredType if available, otherwise declaredType. + */ + public TypeInfo getEffectiveType() { + if (inferredType != null) { + return inferredType; + } + return declaredType; + } + // ==================== BASIC GETTERS ==================== public String getName() { return name; } public Scope getScope() { return scope; } public TypeInfo getDeclaredType() { return declaredType; } - public TypeInfo getTypeInfo() { return declaredType; } // Alias for getDeclaredType + public TypeInfo getTypeInfo() { + // Return the effective type (inferred if available, otherwise declared) + return getEffectiveType(); + } public int getDeclarationOffset() { return declarationOffset; } public boolean isResolved() { return resolved; } public MethodInfo getContainingMethod() { return containingMethod; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 12b9099ea..18faeaadf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -719,7 +719,7 @@ private void extractGlobalFieldInfo(Token token) { } } - TypeInfo declaredType = fieldInfo.getDeclaredType(); + TypeInfo declaredType = fieldInfo.getTypeInfo(); // Try to get modifiers from Java reflection if this is a chained field boolean foundModifiers = false; @@ -825,7 +825,7 @@ private void extractLocalFieldInfo(Token token) { } } - TypeInfo declaredType = fieldInfo.getDeclaredType(); + TypeInfo declaredType = fieldInfo.getTypeInfo(); if (declaredType != null) { int typeColor = getColorForTypeInfo(declaredType); addSegment(getName(declaredType), typeColor); @@ -873,7 +873,7 @@ private void extractParameterInfo(Token token) { } } - TypeInfo declaredType = fieldInfo.getDeclaredType(); + TypeInfo declaredType = fieldInfo.getTypeInfo(); if (declaredType != null) { int typeColor = getColorForTypeInfo(declaredType); addSegment(getName(declaredType), typeColor); @@ -928,7 +928,7 @@ private void buildConstructorDeclaration(MethodInfo constructor, TypeInfo contai for (int i = 0; i < params.size(); i++) { if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); FieldInfo param = params.get(i); - TypeInfo paramType = param.getDeclaredType(); + TypeInfo paramType = param.getTypeInfo(); if (paramType != null) { int paramTypeColor = getColorForTypeInfo(paramType); addSegment(getName(paramType), paramTypeColor); @@ -1025,7 +1025,7 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain for (int i = 0; i < params.size(); i++) { if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor()); FieldInfo param = params.get(i); - TypeInfo paramType = param.getDeclaredType(); + TypeInfo paramType = param.getTypeInfo(); if (paramType != null) { int paramTypeColor = getColorForTypeInfo(paramType); addSegment(getName(paramType), paramTypeColor); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index 86ca3b446..dfafad691 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -362,7 +362,7 @@ public void validateArgTypeError() { FieldInfo para = resolvedMethod.getParameters().get(i); TypeInfo argType = arg.getResolvedType(); - TypeInfo paramType = para.getDeclaredType(); + TypeInfo paramType = para.getTypeInfo(); if (argType != null && paramType != null) { if (!TypeChecker.isTypeCompatible(paramType, argType)) { setArgTypeError(i, "Expected " + paramType.getSimpleName() + diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index b5b1d9b7a..354417161 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -362,7 +362,7 @@ public MethodSignature getSignature() { if (cachedSignature == null) { List paramTypes = new ArrayList<>(); for (FieldInfo param : parameters) - paramTypes.add(param.getDeclaredType()); + paramTypes.add(param.getTypeInfo()); cachedSignature = new MethodSignature(name, paramTypes); } return cachedSignature; @@ -584,7 +584,7 @@ private void validateParameters() { } // Check for unresolved parameter types - TypeInfo paramType = param.getDeclaredType(); + TypeInfo paramType = param.getTypeInfo(); if (paramType == null || !paramType.isResolved()) { String typeName = paramType != null ? paramType.getSimpleName() : "unknown"; addParameterError(param, i, ErrorType.PARAMETER_UNDEFINED, diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java index d79a360ba..afb66fafd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ScriptTypeInfo.java @@ -910,7 +910,7 @@ private boolean parameterTypesMatch(MethodInfo methodInfo, Method javaMethod) { return false; for (int i = 0; i < params.size(); i++) { - TypeInfo paramType = params.get(i).getDeclaredType(); + TypeInfo paramType = params.get(i).getTypeInfo(); if (paramType == null) continue; // Unresolved param, skip check From 5720b3a081a66e6533a6e58a591a2b2a595d37c2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 05:42:12 +0200 Subject: [PATCH 216/337] Added @type JSDOC stub auto generation for fields --- .../client/gui/util/GuiScriptTextArea.java | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) 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 e14995162..0fce2200e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1717,15 +1717,11 @@ private String generateJSDocStub(String after, String indent) { StringBuilder stub = new StringBuilder(); stub.append("\n").append(indent).append(" * "); // Description line - // Try to find a function/method declaration following the JSDoc - // Patterns: - // Java: [modifiers] ReturnType methodName(params) { - // JS: function funcName(params) { or var funcName = function(params) { or funcName(params) { - + // Try to find a function/method declaration or field declaration following the JSDoc // Skip whitespace and newlines String trimmed = after.replaceFirst("^[\\s\\n\\r]*", ""); - // Pattern for Java/JS method/function + // First, try to match function/method pattern java.util.regex.Pattern funcPattern = java.util.regex.Pattern.compile( // Group 1: Optional return type (Java) or 'function' keyword (JS) "^(?:(\\w+(?:<[^>]+>)?|function)\\s+)?" + @@ -1735,11 +1731,12 @@ private String generateJSDocStub(String after, String indent) { "\\(([^)]*)\\)" ); - java.util.regex.Matcher m = funcPattern.matcher(trimmed); - if (m.find()) { - String returnOrKeyword = m.group(1); - String funcName = m.group(2); - String paramsStr = m.group(3) != null ? m.group(3).trim() : ""; + java.util.regex.Matcher funcMatcher = funcPattern.matcher(trimmed); + if (funcMatcher.find()) { + // Handle function/method + String returnOrKeyword = funcMatcher.group(1); + String funcName = funcMatcher.group(2); + String paramsStr = funcMatcher.group(3) != null ? funcMatcher.group(3).trim() : ""; // Determine if this is JavaScript (function keyword or no return type) boolean isJS = "function".equals(returnOrKeyword) || returnOrKeyword == null || @@ -1788,6 +1785,27 @@ private String generateJSDocStub(String after, String indent) { // For JS functions without explicit return type, add empty @returns stub.append("\n").append(indent).append(" * @returns {any}"); } + } else { + // Try to match field declaration: [modifiers] Type name or var/let/const name + java.util.regex.Pattern fieldPattern = java.util.regex.Pattern.compile( + "^(?:(?:public|private|protected|static|final|var|let|const)\\s+)*" + // Optional modifiers + "(\\w+(?:<[^>]+>)?)\\s+" + // Type (Group 1) + "(\\w+)" + // Field name (Group 2) + "(?:\\s*=|\\s*;)" // Followed by = or ; + ); + + java.util.regex.Matcher fieldMatcher = fieldPattern.matcher(trimmed); + if (fieldMatcher.find()) { + String fieldType = fieldMatcher.group(1); + + // Check if this is JavaScript + boolean isJS = container != null && container.getDocument() != null && container.getDocument().isJavaScript(); + + if (isJS && fieldType != null) { + // For JS fields, add @type annotation + stub.append("\n").append(indent).append(" * @type {").append("any").append("}"); + } + } } // Close the JSDoc block From 3929888f6069dce8fab335ab016a4d6a0e0fff59 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 05:43:30 +0200 Subject: [PATCH 217/337] Removed redundant JS type resolution code and improved logic --- .../script/interpreter/ScriptDocument.java | 61 +++---------------- .../script/interpreter/type/TypeInfo.java | 8 ++- .../script/interpreter/type/TypeResolver.java | 33 +++++++--- 3 files changed, 39 insertions(+), 63 deletions(-) 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 5318490de..70bad1e92 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 @@ -1655,24 +1655,16 @@ private String cleanDocumentation(String comment) { } public TypeInfo resolveType(String typeName) { - // Use language-appropriate resolution - if (isJavaScript()) { - return resolveJSTypeUnified(typeName); - } + /** + * Resolve a JS type name to unified TypeInfo. + * Handles JS primitives, .d.ts defined types, and falls back to Java types. + */ + if (isJavaScript()) + return typeResolver.resolveJSType(typeName); + return resolveTypeAndTrackUsage(typeName); } - /** - * Resolve a JS type name to unified TypeInfo. - * Handles JS primitives, .d.ts defined types, and falls back to Java types. - */ - private TypeInfo resolveJSTypeUnified(String typeName) { - if (typeName == null || typeName.isEmpty()) { - return TypeInfo.fromPrimitive("void"); - } - return typeResolver.resolveJSType(typeName); - } - /** * Resolve a type and track the import usage for unused import detection. * Checks script-defined types first, then falls back to imported types. @@ -2455,45 +2447,6 @@ private void markNonJSDocComments(List marks) { marks.add(new ScriptLine.Mark(start, end, TokenType.COMMENT, null)); } } - - /** - * Resolve a type name from JSDoc to a TypeInfo. - */ - private TypeInfo resolveJSDocType(String typeName) { - if (typeName == null || typeName.isEmpty()) { - return null; - } - - // Handle union types - use first type - if (typeName.contains("|")) { - typeName = typeName.split("\\|")[0].trim(); - } - - // Handle nullable - typeName = typeName.replace("?", "").trim(); - - // Map JSDoc types to Java types - switch (typeName.toLowerCase()) { - case "string": - return TypeInfo.fromClass(String.class); - case "number": - case "int": - case "integer": - return isJavaScript() ? TypeInfo.fromClass(double.class) : TypeInfo.fromClass(int.class); - case "boolean": - case "bool": - return TypeInfo.fromClass(boolean.class); - case "void": - return TypeInfo.fromPrimitive("void"); - case "any": - case "object": - case "*": - return TypeInfo.ANY; - } - - // Try through the type resolver - return resolveType(typeName); - } private void markMethodDeclarations(List marks) { Matcher m = METHOD_DECL_PATTERN.matcher(text); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 28f307187..cc192e38c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -37,13 +37,19 @@ public enum Kind { */ public static final TypeInfo VOID = fromPrimitive("void"); + public static final TypeInfo BOOLEAN = fromPrimitive("boolean"); + + public static final TypeInfo STRING = TypeInfo.fromClass(String.class); + /** * Singleton constant for the "any" type (used in JavaScript/TypeScript). * The "any" type is universally compatible - it can be assigned to anything * and anything can be assigned to it. */ public static final TypeInfo ANY = new TypeInfo("any", "any", "", Kind.CLASS, null, true, null); - + public static final TypeInfo NUMBER = new TypeInfo("number", "number", "", Kind.CLASS, double.class, true, null); + + private final String simpleName; // e.g., "List", "ColorType" private final String fullName; // e.g., "java.util.List", "kamkeel...IOverlay$ColorType" private final String packageName; // e.g., "java.util", "kamkeel.npcdbc.api.client.overlay" 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 297256f97..228c26040 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 @@ -136,18 +136,35 @@ public TypeInfo resolveJSType(String jsTypeName) { if (jsTypeName == null || jsTypeName.isEmpty()) { return TypeInfo.fromPrimitive("void"); } + + // Handle union types - use first type + if (jsTypeName.contains("|")) { + jsTypeName = jsTypeName.split("\\|")[0].trim(); + } + + // Handle nullable + jsTypeName = jsTypeName.replace("?", "").trim(); // Strip array brackets for base type resolution boolean isArray = jsTypeName.endsWith("[]"); String baseName = isArray ? jsTypeName.substring(0, jsTypeName.length() - 2) : jsTypeName; - - // Check JS primitives first - if (JS_PRIMITIVE_TO_JAVA.containsKey(baseName)) { - String javaType = JS_PRIMITIVE_TO_JAVA.get(baseName); - TypeInfo baseType = TypeResolver.isPrimitiveType(javaType) - ? TypeInfo.fromPrimitive(javaType) - : resolveFullName("java.lang." + javaType); - return isArray ? TypeInfo.arrayOf(baseType) : baseType; + + switch (baseName.toLowerCase()) { + case "string": + return TypeInfo.STRING; + case "number": + case "int": + case "integer": + return TypeInfo.NUMBER; + case "boolean": + case "bool": + return TypeInfo.BOOLEAN; + case "void": + return TypeInfo.VOID; + case "any": + case "object": + case "*": + return TypeInfo.ANY; } // Check JS type registry From ce3700b820588e0e968fc47e7d48c70e986cbab7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 23:39:06 +0200 Subject: [PATCH 218/337] Fixed hoverinfo not showing on JS method declarations --- .../npcs/client/gui/util/script/interpreter/ScriptDocument.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 70bad1e92..35b9e71e3 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 @@ -2465,7 +2465,7 @@ private void markMethodDeclarations(List marks) { int methodDeclStart = m.start(); MethodInfo methodInfo = null; for (MethodInfo method : getAllMethods()) { - if (method.getDeclarationOffset() == methodDeclStart) { + if (method.getFullDeclarationOffset() == methodDeclStart) { methodInfo = method; break; } From 830f2a3d8faae294ff87a9da95a362f2cf9f873e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 23:41:48 +0200 Subject: [PATCH 219/337] Stored JSDocInfo in FieldInfo and MethodInfo --- .../gui/util/script/interpreter/ScriptDocument.java | 9 +++++++++ .../gui/util/script/interpreter/field/FieldInfo.java | 6 +++++- .../gui/util/script/interpreter/method/MethodInfo.java | 4 ++++ 3 files changed, 18 insertions(+), 1 deletion(-) 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 35b9e71e3..37839e2c3 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 @@ -738,6 +738,10 @@ private void parseMethodDeclarations() { m.start(), nameStart, nameStart, bodyStart, bodyEnd, 0, documentation ); + + if(jsDoc!=null) + methodInfo.setJSDocInfo(jsDoc); + methods.add(methodInfo); } } else { @@ -1032,6 +1036,8 @@ private void parseLocalVariables() { FieldInfo fieldInfo = FieldInfo.localField(varName, typeInfo, absPos, method, initStart, initEnd, 0); + if(jsDoc != null) + fieldInfo.setJSDocInfo(jsDoc); // Check for duplicate if (locals.containsKey(varName) || globalFields.containsKey(varName)) { AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( @@ -1349,6 +1355,9 @@ private void parseGlobalFields() { FieldInfo fieldInfo = FieldInfo.globalField(varName, typeInfo, position, documentation, initStart, initEnd, 0); + if(jsDoc != null) + fieldInfo.setJSDocInfo(jsDoc); + if (globalFields.containsKey(varName)) { AssignmentInfo dupError = AssignmentInfo.duplicateDeclaration( varName, position, position + varName.length(), diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 5d6b6efdd..9182cc267 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -3,6 +3,7 @@ import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -34,7 +35,8 @@ public enum Scope { private final int declarationOffset; // Where this field was declared in the source private final boolean resolved; private final String documentation; // Javadoc/comment documentation for this field - + private JSDocInfo jsDocInfo; // Parsed JSDoc info for this method (may be null) + // Initialization value range (for displaying "= value" in hover info) private final int initStart; // Position of '=' or -1 if no initializer private final int initEnd; // Position after initializer (before ';') or -1 @@ -467,6 +469,8 @@ public TypeInfo getTypeInfo() { public boolean isResolved() { return resolved; } public MethodInfo getContainingMethod() { return containingMethod; } public String getDocumentation() { return documentation; } + public JSDocInfo getJSDocInfo() { return jsDocInfo; } + public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; } public int getInitStart() { return initStart; } public int getInitEnd() { return initEnd; } public boolean hasInitializer() { return initStart >= 0 && initEnd > initStart; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 354417161..e266ac824 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -2,6 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.*; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; @@ -59,6 +60,7 @@ public enum ErrorType { private final boolean isDeclaration; // true if this is a declaration, false if it's a call private final int modifiers; // Java Modifier flags (e.g., Modifier.PUBLIC | Modifier.STATIC) private final String documentation; // Javadoc/comment documentation for this method + private JSDocInfo jsDocInfo; // Parsed JSDoc info for this method (may be null) private final java.lang.reflect.Method javaMethod; // The Java reflection Method, if this was created from reflection // Error tracking for method declarations @@ -269,6 +271,8 @@ private static TypeInfo resolveJSType(String jsTypeName) { public boolean isPrivate() { return Modifier.isPrivate(modifiers); } public boolean isProtected() { return Modifier.isProtected(modifiers); } public String getDocumentation() { return documentation; } + public JSDocInfo getJSDocInfo() { return jsDocInfo; } + public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; } // ==================== OVERRIDE/IMPLEMENTS GETTERS/SETTERS ==================== From 4741b58aedde03dcd84de473fff44c0796ece86f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 23:42:20 +0200 Subject: [PATCH 220/337] Fixed description not being stored for JSDocTypeTag --- .../gui/util/script/interpreter/jsdoc/JSDocParser.java | 9 +++++---- .../gui/util/script/interpreter/jsdoc/JSDocTypeTag.java | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java index e93ed12eb..2d97d257e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java @@ -18,13 +18,13 @@ public class JSDocParser { "/\\*\\*([\\s\\S]*?)\\*/", Pattern.MULTILINE); private static final Pattern TYPE_TAG_PATTERN = Pattern.compile( - "@(type)\\s*\\{([^}]+)\\}", Pattern.CASE_INSENSITIVE); + "@(type)\\s*\\{([^}]+)\\}(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); private static final Pattern PARAM_TAG_PATTERN = Pattern.compile( - "@(param)\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)(?:\\s*-?\\s*(.*))?", Pattern.CASE_INSENSITIVE); + "@(param)\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); private static final Pattern RETURN_TAG_PATTERN = Pattern.compile( - "@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?", Pattern.CASE_INSENSITIVE); + "@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); private static final Pattern ANY_TAG_PATTERN = Pattern.compile( "@(\\w+)", Pattern.CASE_INSENSITIVE); @@ -105,6 +105,7 @@ private void parseTypeTag(String content, int contentOffset, JSDocInfo info) { int tagNameEnd = contentOffset + m.end(1); String typeName = m.group(2).trim(); + String description = m.group(3); int braceStart = content.indexOf('{', m.start()); int braceEnd = content.indexOf('}', braceStart); @@ -114,7 +115,7 @@ private void parseTypeTag(String content, int contentOffset, JSDocInfo info) { TypeInfo typeInfo = resolveType(typeName); JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd, - typeName, typeInfo, typeStart, typeEnd); + typeName, typeInfo, typeStart, typeEnd, description != null ? description.trim() : null); info.setTypeTag(tag); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java index 0ec2c1389..1f27f6667 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocTypeTag.java @@ -13,10 +13,11 @@ public JSDocTypeTag(int atSignOffset, int tagNameStart, int tagNameEnd) { super("type", atSignOffset, tagNameStart, tagNameEnd); } - public static JSDocTypeTag create(int atSignOffset, int tagNameStart, int tagNameEnd, - String typeName, TypeInfo typeInfo, int typeStart, int typeEnd) { + public static JSDocTypeTag create(int atSignOffset, int tagNameStart, int tagNameEnd, String typeName, + TypeInfo typeInfo, int typeStart, int typeEnd, String description) { JSDocTypeTag tag = new JSDocTypeTag(atSignOffset, tagNameStart, tagNameEnd); tag.setType(typeName, typeInfo, typeStart, typeEnd); + tag.setDescription(description); return tag; } From 9b1ffb4992e2af17208b6e9577c001d366d81de7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 23:46:46 +0200 Subject: [PATCH 221/337] -Fixed JSDocTypeTag type not being marked properly -Matched marking of JSDocParamTag name with equivalent method parameter (if exists, else UNDEFINED) --- .../script/interpreter/ScriptDocument.java | 118 +++++++++++++++++- 1 file changed, 114 insertions(+), 4 deletions(-) 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 37839e2c3..7d72fd632 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 @@ -2380,19 +2380,70 @@ private void markJSDocElements(List marks) { } String commentContent = jsDocMatcher.group(0); + + // Find the method that this JSDoc belongs to, for parameter validation + MethodInfo associatedMethod = findMethodAfterPosition(commentEnd); + Set methodParamNames = new HashSet<>(); + if (associatedMethod != null) { + for (FieldInfo param : associatedMethod.getParameters()) { + methodParamNames.add(param.getName()); + } + } // Collect all special element positions (@tags and {Type}s) that should NOT be gray java.util.List specialRanges = new java.util.ArrayList<>(); - - // Find @tags + + // Find @tags and process @param and @type specially Pattern tagPattern = Pattern.compile("@(\\w+)"); Matcher tagMatcher = tagPattern.matcher(commentContent); while (tagMatcher.find()) { int tagStart = commentStart + tagMatcher.start(); int tagEnd = commentStart + tagMatcher.end(); specialRanges.add(new int[]{tagStart, tagEnd}); - // Mark the @tag itself - marks.add(new ScriptLine.Mark(tagStart, tagEnd, TokenType.JSDOC_TAG, null)); + + String tagName = tagMatcher.group(1); + + // For @type tag, resolve the type and attach it for hover + TypeInfo tagTypeInfo = null; + if ("type".equals(tagName)) { + // Look for {Type} after @type + int afterTag = tagMatcher.end(); + String afterTagText = commentContent.substring(afterTag); + Pattern typeRefPattern = Pattern.compile("^\\s*\\{([^}]+)\\}"); + Matcher typeRefMatcher = typeRefPattern.matcher(afterTagText); + if (typeRefMatcher.find()) { + String typeName = typeRefMatcher.group(1).trim(); + tagTypeInfo = resolveType(typeName); + } + } + + // Mark the @tag itself (with TypeInfo for @type) + marks.add(new ScriptLine.Mark(tagStart, tagEnd, TokenType.JSDOC_TAG, tagTypeInfo)); + + // If this is @param, look for parameter name after the type + if ("param".equals(tagName)) { + // Look for: @param {Type} paramName or @param paramName + int afterTag = tagMatcher.end(); + String afterTagText = commentContent.substring(afterTag); + + // Pattern to match optional {Type} followed by parameter name + Pattern paramNamePattern = Pattern.compile("^\\s*(?:\\{[^}]*\\}\\s*)?([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher paramNameMatcher = paramNamePattern.matcher(afterTagText); + if (paramNameMatcher.find()) { + String paramName = paramNameMatcher.group(1); + int paramNameStart = commentStart + afterTag + paramNameMatcher.start(1); + int paramNameEnd = commentStart + afterTag + paramNameMatcher.end(1); + + // Check if this parameter exists in the method + boolean paramExists = methodParamNames.contains(paramName); + TokenType paramTokenType = paramExists ? TokenType.PARAMETER : TokenType.UNDEFINED_VAR; + + // Add to special ranges to exclude from comment marking + specialRanges.add(new int[]{paramNameStart, paramNameEnd}); + // Mark the parameter name + marks.add(new ScriptLine.Mark(paramNameStart, paramNameEnd, paramTokenType, null)); + } + } } // Find {Type} references @@ -2435,6 +2486,65 @@ private void markJSDocElements(List marks) { } } } + + /** + * Find the method declaration that immediately follows the given position. + * Used to associate JSDoc comments with their methods for parameter validation. + */ + private MethodInfo findMethodAfterPosition(int position) { + // Skip whitespace after position + int searchStart = position; + while (searchStart < text.length() && Character.isWhitespace(text.charAt(searchStart))) { + searchStart++; + } + + // Find the method with the smallest offset that is >= searchStart + MethodInfo closestMethod = null; + int closestDistance = Integer.MAX_VALUE; + + for (MethodInfo method : methods) { + if (!method.isDeclaration()) + continue; + int methodStart = method.getFullDeclarationOffset(); + if (methodStart < 0) + methodStart = method.getNameOffset(); + if (methodStart < 0) + continue; + + // Check if this method starts at or after our search position + // but within a reasonable distance (e.g., 200 chars to account for modifiers) + if (methodStart >= position && methodStart < position + 200) { + int distance = methodStart - position; + if (distance < closestDistance) { + closestDistance = distance; + closestMethod = method; + } + } + } + + // Also check methods inside script types + for (ScriptTypeInfo scriptType : scriptTypes.values()) { + for (MethodInfo method : scriptType.getAllMethodsFlat()) { + if (!method.isDeclaration()) + continue; + int methodStart = method.getFullDeclarationOffset(); + if (methodStart < 0) + methodStart = method.getNameOffset(); + if (methodStart < 0) + continue; + + if (methodStart >= position && methodStart < position + 200) { + int distance = methodStart - position; + if (distance < closestDistance) { + closestDistance = distance; + closestMethod = method; + } + } + } + } + + return closestMethod; + } /** * Mark all comments EXCEPT JSDoc comments (slash-star-star style). From edffd7aaaf87c5999a38b8f8c7f5b1eab74407f2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 5 Jan 2026 23:49:49 +0200 Subject: [PATCH 222/337] JSDoc tag rendering for HoverInfo --- .../interpreter/hover/TokenHoverInfo.java | 166 +++++++++++++++++- .../interpreter/hover/TokenHoverRenderer.java | 57 +++++- 2 files changed, 216 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 18faeaadf..40da28d3f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -5,6 +5,10 @@ import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTypeTag; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; @@ -46,6 +50,9 @@ public class TokenHoverInfo { /** Javadoc/documentation comment lines */ private List documentation = new ArrayList<>(); + + /** JSDoc-formatted documentation with colored segments (sections like Params, Returns) */ + private List jsDocLines = new ArrayList<>(); /** Error messages (shown in red) */ private List errors = new ArrayList<>(); @@ -56,6 +63,32 @@ public class TokenHoverInfo { /** The token this info was built from */ private final Token token; + // ==================== DOCUMENTATION LINE ==================== + + /** + * A line of documentation that may contain colored segments. + * Used for JSDoc-style rendering with "Params:", parameter names, etc. + */ + public static class DocumentationLine { + public final List segments; + + public DocumentationLine() { + this.segments = new ArrayList<>(); + } + + public void addSegment(String text, int color) { + segments.add(new TextSegment(text, color)); + } + + public void addText(String text) { + segments.add(new TextSegment(text, TextSegment.COLOR_DEFAULT)); + } + + public boolean isEmpty() { + return segments.isEmpty() || segments.stream().allMatch(s -> s.text == null || s.text.isEmpty()); + } + } + // ==================== TEXT SEGMENT ==================== /** @@ -712,7 +745,10 @@ private void extractGlobalFieldInfo(Token token) { iconIndicator = "f"; // Add documentation if available - if (fieldInfo.getDocumentation() != null && !fieldInfo.getDocumentation().isEmpty()) { + JSDocInfo jsDoc = fieldInfo.getJSDocInfo(); + if (jsDoc != null) { + formatJSDocumentation(jsDoc, null); + } else if (fieldInfo.getDocumentation() != null && !fieldInfo.getDocumentation().isEmpty()) { String[] docLines = fieldInfo.getDocumentation().split("\n"); for (String line : docLines) { documentation.add(line); @@ -1035,8 +1071,12 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain } addSegment(")", TokenType.DEFAULT.getHexColor()); - // Add documentation if available - if (methodInfo.getDocumentation() != null && !methodInfo.getDocumentation().isEmpty()) { + // Add documentation if available - use JSDoc formatting if we have JSDocInfo + JSDocInfo jsDoc = methodInfo.getJSDocInfo(); + if (jsDoc != null) { + formatJSDocumentation(jsDoc, methodInfo.getParameters()); + } else if (methodInfo.getDocumentation() != null && !methodInfo.getDocumentation().isEmpty()) { + // Fallback to raw documentation String[] docLines = methodInfo.getDocumentation().split("\n"); for (String line : docLines) { documentation.add(line); @@ -1044,6 +1084,116 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain } } + + /** + * Format JSDoc information in IntelliJ-style with "Params:" and "Returns:" sections. + * Creates colored documentation lines with parameter names highlighted. + */ + private void formatJSDocumentation(JSDocInfo jsDoc, List methodParams) { + // Add description if available (without @tags) + String description = jsDoc.getDescription(); + if (description != null && !description.isEmpty()) { + // Clean up the description - remove leading/trailing whitespace and asterisks + String[] descLines = description.split("\n"); + for (String line : descLines) { + line = line.trim(); + if (line.startsWith("*")) { + line = line.substring(1).trim(); + } + if (!line.isEmpty() && !line.startsWith("@")) { + documentation.add(line); + } + } + } + + // Add Returns section if there's a @return tag + JSDocTypeTag typeTag = jsDoc.getTypeTag(); + if (typeTag != null) { + // "Type:" header + DocumentationLine typeLine = new DocumentationLine(); + typeLine.addSegment("Type:", TokenType.JSDOC_TAG.getHexColor()); + + // Type if available + if (typeTag.hasType()) { + typeLine.addText(" "); + typeLine.addSegment("{", TokenType.JSDOC_TYPE.getHexColor()); + typeLine.addSegment(typeTag.getTypeName(), TokenType.getColor(typeTag.getTypeInfo())); + typeLine.addSegment("}", TokenType.JSDOC_TYPE.getHexColor()); + } + + // Description if available + String typeDesc = typeTag.getDescription(); + if (typeDesc != null && !typeDesc.isEmpty()) { + typeLine.addText(" - "); + typeLine.addText(typeDesc.trim()); + } + + jsDocLines.add(typeLine); + } + + // Add Params section if there are @param tags + List paramTags = jsDoc.getParamTags(); + if (paramTags != null && !paramTags.isEmpty()) { + // "Params:" header + DocumentationLine paramsHeader = new DocumentationLine(); + paramsHeader.addSegment("Params:", TokenType.JSDOC_TAG.getHexColor()); + jsDocLines.add(paramsHeader); + + // Add each parameter + for (JSDocParamTag paramTag : paramTags) { + DocumentationLine paramLine = new DocumentationLine(); + + // Indent and parameter name + String paramName = paramTag.getParamName(); + boolean paramExists = methodParams != null && methodParams.stream() + .anyMatch(p -> p.getName().equals(paramName)); + paramLine.addSegment(paramName, + paramExists ? TokenType.PARAMETER.getHexColor() : TokenType.UNDEFINED_VAR.getHexColor()); + + // Type if available + if (paramTag.hasType()) { + paramLine.addSegment(" {", TokenType.JSDOC_TYPE.getHexColor()); + paramLine.addSegment(paramTag.getTypeName(), TokenType.getColor(paramTag.getTypeInfo())); + paramLine.addSegment("}", TokenType.JSDOC_TYPE.getHexColor()); + } + + // Description if available + String paramDesc = paramTag.getDescription(); + if (paramDesc != null && !paramDesc.isEmpty()) { + paramLine.addText(" - "); + paramLine.addText(paramDesc.trim()); + } + + jsDocLines.add(paramLine); + } + } + + // Add Returns section if there's a @return tag + JSDocReturnTag returnTag = jsDoc.getReturnTag(); + if (returnTag != null) { + DocumentationLine returnLine = new DocumentationLine(); + + //"Returns:" header + returnLine.addSegment("Returns:", TokenType.JSDOC_TAG.getHexColor()); + + // Type if available + if (returnTag.hasType()) { + returnLine.addText(" "); + returnLine.addSegment("{", TokenType.JSDOC_TYPE.getHexColor()); + returnLine.addSegment(returnTag.getTypeName(), TokenType.getColor(returnTag.getTypeInfo())); + returnLine.addSegment("}", TokenType.JSDOC_TYPE.getHexColor()); + } + + // Description if available + String returnDesc = returnTag.getDescription(); + if (returnDesc != null && !returnDesc.isEmpty()) { + returnLine.addText(" - "); + returnLine.addText(returnDesc.trim()); + } + + jsDocLines.add(returnLine); + } + } private void extractJavadoc(Method method) { // Java reflection doesn't provide Javadoc at runtime @@ -1242,12 +1392,20 @@ public List getDeclaration() { return declaration; } public List getDocumentation() { return documentation; } + + public List getJSDocLines() { + return jsDocLines; + } public List getErrors() { return errors; } public List getAdditionalInfo() { return additionalInfo; } public Token getToken() { return token; } public boolean hasContent() { - return !declaration.isEmpty() || !errors.isEmpty() || !documentation.isEmpty(); + return !declaration.isEmpty() || !errors.isEmpty() || !documentation.isEmpty() || !jsDocLines.isEmpty(); + } + + public boolean hasJSDocContent() { + return !jsDocLines.isEmpty(); } public boolean hasErrors() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index 4903ddb9a..4af1352cb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -203,7 +203,8 @@ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, currentY = drawWrappedSegments(textX, currentY, wrapWidth, declaration); currentY += LINE_SPACING; } - // Draw documentation + + // Draw documentation (plain text) if (!docs.isEmpty()) { // Draw separator line before documentation Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR); @@ -217,6 +218,26 @@ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, } } + // Draw JSDoc-formatted documentation with colored segments + List jsDocLines = info.getJSDocLines(); + if (!jsDocLines.isEmpty()) { + // Draw separator line if there was plain documentation or declaration + if (docs.isEmpty() && !declaration.isEmpty()) { + Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR); + currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING; + } + + for (TokenHoverInfo.DocumentationLine docLine : jsDocLines) { + if (!docLine.isEmpty()) { + currentY = drawWrappedSegments(textX, currentY, wrapWidth, docLine.segments); + currentY += LINE_SPACING; + } else { + // Empty line - just add spacing + currentY += lineHeight + LINE_SPACING; + } + } + } + // Draw additional info if (!additionalInfo.isEmpty()) { @@ -368,6 +389,17 @@ private static int calculateContentWidth(TokenHoverInfo info, int maxWidth) { } } + // JSDoc-formatted documentation lines + List jsDocLines = info.getJSDocLines(); + if (jsDocLines != null) { + for (TokenHoverInfo.DocumentationLine docLine : jsDocLines) { + if (!docLine.isEmpty()) { + int lineLongest = calculateSegmentsLongestLine(maxWidth, docLine.segments); + longestLineWidth = Math.max(longestLineWidth, lineLongest); + } + } + } + // Additional info for (String line : info.getAdditionalInfo()) { List wrappedLines = wrapText(line, maxWidth); @@ -391,6 +423,7 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) String packageName = info.getPackageName(); List declaration = info.getDeclaration(); List docs = info.getDocumentation(); + List jsDocLines = info.getJSDocLines(); List additionalInfo = info.getAdditionalInfo(); // Errors @@ -402,7 +435,7 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) } boolean onlyErrors = errors != null && !errors.isEmpty() && (packageName == null || packageName.isEmpty()) && (declaration == null || - declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty()); + declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (jsDocLines == null || jsDocLines.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty()); if (!onlyErrors) //Add error separator height totalHeight += (SEPARATOR_SPACING + SEPARATOR_HEIGHT) * 2 - 5; @@ -423,7 +456,7 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) totalHeight += LINE_SPACING; } - // Documentation + // Documentation (plain text) if (!docs.isEmpty()) { // Add space for separator line and spacing totalHeight += SEPARATOR_SPACING + SEPARATOR_HEIGHT; @@ -433,6 +466,24 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) } } + // JSDoc-formatted documentation lines + if (jsDocLines != null && !jsDocLines.isEmpty()) { + // Add separator if there was plain documentation or declaration but no plain docs + if (docs.isEmpty() && !declaration.isEmpty()) { + // totalHeight += SEPARATOR_SPACING + SEPARATOR_HEIGHT; + } + + for (TokenHoverInfo.DocumentationLine docLine : jsDocLines) { + if (!docLine.isEmpty()) { + totalHeight += calculateSegmentsHeight(contentWidth, docLine.segments); + //. totalHeight += LINE_SPACING; + } else { + // Empty line + // totalHeight += lineHeight + LINE_SPACING; + } + } + } + // Additional info if (!additionalInfo.isEmpty()) { totalHeight += LINE_SPACING; From 3ee6b69eb11c24ff5679d5b1cab64b85873303b3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 6 Jan 2026 00:15:54 +0200 Subject: [PATCH 223/337] renamerefactor aint workin for JSDoc parameter name --- .../util/script/RenameRefactorHandler.java | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 4f36da54b..3336e0867 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -34,6 +34,10 @@ public class RenameRefactorHandler { private List allOccurrences = new ArrayList<>(); private ScopeInfo scope = null; + // For parameter renaming: track containing method to find JSDoc + private boolean isParameterRename = false; + private MethodBlock containingMethodBlock = null; + // For global scope, track positions that have local shadowing (to exclude them) private List localShadowedPositions = new ArrayList<>(); @@ -145,13 +149,28 @@ public boolean startRename() { // Determine scope for this identifier - MUST check if local shadows global scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer()); + // Track if this is a parameter rename and store the containing method + isParameterRename = false; + containingMethodBlock = null; + if (scope != null && "parameter".equals(scope.scopeType)) { + isParameterRename = true; + // Find the containing method block + List methods = MethodBlock.collectMethodBlocks(text); + for (MethodBlock method : methods) { + if (method.containsPosition(wordBounds[0])) { + containingMethodBlock = method; + break; + } + } + } + // If global scope, find all positions where local variables shadow this name localShadowedPositions.clear(); if (scope != null && scope.isGlobal) { findLocalShadowedPositions(text, originalWord); } - // Find all occurrences within scope + // Find all occurrences within scope (including JSDoc if it's a parameter) findOccurrences(text, originalWord); if (allOccurrences.isEmpty()) @@ -231,6 +250,8 @@ private void resetState() { allOccurrences.clear(); localShadowedPositions.clear(); scope = null; + isParameterRename = false; + containingMethodBlock = null; originalText = ""; originalCursorPos = 0; } @@ -442,6 +463,23 @@ private void applyLiveRename() { // Update tracking for current word (only when non-empty) originalWord = currentWord; + // For parameter renames, update the containing method block position + if (isParameterRename && containingMethodBlock != null) { + // Calculate how much the method shifted + int methodShift = 0; + for (int[] occ : allOccurrences) { + if (occ[1] <= containingMethodBlock.startOffset) { + methodShift += lengthDiff; + } + } + // Update method block position for next JSDoc search + containingMethodBlock = new MethodBlock( + containingMethodBlock.startOffset + methodShift, + containingMethodBlock.endOffset + methodShift + (allOccurrences.size() * lengthDiff) - methodShift, + containingMethodBlock.text + ); + } + // Recalculate occurrences based on the new word findOccurrences(newText, currentWord); @@ -536,6 +574,62 @@ private void findOccurrences(String text, String word) { allOccurrences.add(new int[]{start, end}); } } + + // If this is a parameter rename, also look for JSDoc @param occurrences + if (isParameterRename && containingMethodBlock != null) { + findJSDocParamOccurrences(text, word); + } + } + + /** + * Find JSDoc @param occurrences for a parameter name. + * Looks for JSDoc comments immediately before the method's scope and finds @param tags. + */ + private void findJSDocParamOccurrences(String text, String paramName) { + if (containingMethodBlock == null) return; + + // Look backwards from the method start for a JSDoc comment + int methodStart = containingMethodBlock.startOffset; + + // Find JSDoc comment ending just before the method + // JSDoc pattern: /** ... */ + Pattern jsDocPattern = Pattern.compile("/\\*\\*([\\s\\S]*?)\\*/"); + Matcher jsDocMatcher = jsDocPattern.matcher(text); + + int jsDocStart = -1; + String jsDocContent = null; + + while (jsDocMatcher.find()) { + int end = jsDocMatcher.end(); + // Check if this JSDoc is right before the method (within 200 chars, allowing for whitespace/function keywords) + if (end <= methodStart && methodStart - end < 200) { + // Check if there's no other code between JSDoc and method + // For JavaScript: allow "function", "async", "var/let/const name = ", etc. + String between = text.substring(end, methodStart).trim(); + // Allow empty, or typical JS function declaration patterns + if (between.isEmpty() || + between.matches("^(?:async\\s+)?(?:function\\s+\\w*)?\\s*$") || + between.matches("^(?:var|let|const)\\s+\\w+\\s*=\\s*(?:async\\s+)?(?:function)?\\s*$") || + between.matches("^\\w+\\s*[=:]\\s*(?:async\\s+)?(?:function)?\\s*$") || + between.matches("^(?:public|private|protected|static|final|abstract|synchronized|native|transient|volatile|\\s)+$")) { + jsDocStart = jsDocMatcher.start(); + jsDocContent = jsDocMatcher.group(0); + } + } + } + + if (jsDocContent == null) return; + + // Find @param paramName in the JSDoc + // Pattern: @param (optional {Type}) paramName + Pattern paramPattern = Pattern.compile("@param\\s+(?:\\{[^}]*\\}\\s+)?(" + Pattern.quote(paramName) + ")(?:\\s|$|-)"); + Matcher paramMatcher = paramPattern.matcher(jsDocContent); + + while (paramMatcher.find()) { + int paramNameStart = jsDocStart + paramMatcher.start(1); + int paramNameEnd = jsDocStart + paramMatcher.end(1); + allOccurrences.add(new int[]{paramNameStart, paramNameEnd}); + } } /** From 4726c23ba1e8abdca9e3b87719617c0801b0a283 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 6 Jan 2026 02:26:27 +0200 Subject: [PATCH 224/337] Got refactoring func param name working for JSDoc param name --- .../client/gui/util/GuiScriptTextArea.java | 2 +- .../client/gui/util/GuiScriptTextArea1.java | 2 +- .../util/script/RenameRefactorHandler.java | 145 ++++++++++-------- 3 files changed, 81 insertions(+), 68 deletions(-) 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 0fce2200e..5ab759183 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -448,7 +448,7 @@ public void scrollToPosition(int pos) { } @Override - public JavaTextContainer getContainer() { + public ScriptTextContainer getContainer() { return container; } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java index fbf439767..60e5a38a0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea1.java @@ -417,7 +417,7 @@ public void scrollToPosition(int pos) { } @Override - public JavaTextContainer getContainer() { + public ScriptTextContainer getContainer() { return container; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 3336e0867..2193c20d5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -1,8 +1,13 @@ package noppes.npcs.client.gui.util.script; import net.minecraft.client.gui.GuiScreen; -import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import org.lwjgl.input.Keyboard; import java.util.ArrayList; @@ -36,7 +41,7 @@ public class RenameRefactorHandler { // For parameter renaming: track containing method to find JSDoc private boolean isParameterRename = false; - private MethodBlock containingMethodBlock = null; + private MethodInfo containingMethod = null; // For global scope, track positions that have local shadowing (to exclude them) private List localShadowedPositions = new ArrayList<>(); @@ -87,7 +92,7 @@ public interface RenameCallback { void scrollToPosition(int pos); - JavaTextContainer getContainer(); + ScriptTextContainer getContainer(); int getViewportWidth(); // For determining if clicks are in rename box } @@ -149,17 +154,35 @@ public boolean startRename() { // Determine scope for this identifier - MUST check if local shadows global scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer()); - // Track if this is a parameter rename and store the containing method + // Track if this is a parameter rename by checking if position is in any method's parameter declaration isParameterRename = false; - containingMethodBlock = null; - if (scope != null && "parameter".equals(scope.scopeType)) { - isParameterRename = true; - // Find the containing method block - List methods = MethodBlock.collectMethodBlocks(text); - for (MethodBlock method : methods) { - if (method.containsPosition(wordBounds[0])) { - containingMethodBlock = method; - break; + containingMethod = null; + + ScriptTextContainer container = callback.getContainer(); + if (container != null && container.getDocument() != null) { + ScriptDocument document = container.getDocument(); + List methods = document.getAllMethods(); + + // Check if the word at cursor position is a parameter of any method + // A rename is a parameter rename if: + // 1. The word matches a parameter name + // 2. The cursor is within that method's declaration or body + for (MethodInfo method : methods) { + // Check if cursor is within this method's range + int methodStart = method.getFullDeclarationOffset(); + int methodEnd = method.getBodyEnd(); + if (methodStart < 0 || methodEnd < 0) continue; // External method + + if (wordBounds[0] >= methodStart && wordBounds[0] <= methodEnd) { + // Check if the word matches any parameter of this method + for (FieldInfo param : method.getParameters()) { + if (param.getName().equals(originalWord)) { + isParameterRename = true; + containingMethod = method; + break; + } + } + if (isParameterRename) break; } } } @@ -251,7 +274,7 @@ private void resetState() { localShadowedPositions.clear(); scope = null; isParameterRename = false; - containingMethodBlock = null; + containingMethod = null; originalText = ""; originalCursorPos = 0; } @@ -463,23 +486,6 @@ private void applyLiveRename() { // Update tracking for current word (only when non-empty) originalWord = currentWord; - // For parameter renames, update the containing method block position - if (isParameterRename && containingMethodBlock != null) { - // Calculate how much the method shifted - int methodShift = 0; - for (int[] occ : allOccurrences) { - if (occ[1] <= containingMethodBlock.startOffset) { - methodShift += lengthDiff; - } - } - // Update method block position for next JSDoc search - containingMethodBlock = new MethodBlock( - containingMethodBlock.startOffset + methodShift, - containingMethodBlock.endOffset + methodShift + (allOccurrences.size() * lengthDiff) - methodShift, - containingMethodBlock.text - ); - } - // Recalculate occurrences based on the new word findOccurrences(newText, currentWord); @@ -576,58 +582,65 @@ private void findOccurrences(String text, String word) { } // If this is a parameter rename, also look for JSDoc @param occurrences - if (isParameterRename && containingMethodBlock != null) { + if (isParameterRename && containingMethod != null) { findJSDocParamOccurrences(text, word); } } /** * Find JSDoc @param occurrences for a parameter name. - * Looks for JSDoc comments immediately before the method's scope and finds @param tags. + * Uses the JSDocInfo stored in the MethodInfo to locate the JSDoc comment, + * then searches for the parameter name pattern in the current text. */ private void findJSDocParamOccurrences(String text, String paramName) { - if (containingMethodBlock == null) return; + if (containingMethod == null) return; - // Look backwards from the method start for a JSDoc comment - int methodStart = containingMethodBlock.startOffset; + // Get the JSDocInfo from the method to verify it has a @param for our parameter + JSDocInfo jsDocInfo = containingMethod.getJSDocInfo(); + if (jsDocInfo == null) return; - // Find JSDoc comment ending just before the method - // JSDoc pattern: /** ... */ - Pattern jsDocPattern = Pattern.compile("/\\*\\*([\\s\\S]*?)\\*/"); - Matcher jsDocMatcher = jsDocPattern.matcher(text); + // Check that this parameter has a JSDoc @param tag (using initial word to find the tag definition) + JSDocParamTag paramTag = jsDocInfo.getParamTag(initialWord); + if (paramTag == null) return; - int jsDocStart = -1; - String jsDocContent = null; + // Get the approximate JSDoc location from the original document + int originalJsDocStart = jsDocInfo.getStartOffset(); + int originalJsDocEnd = jsDocInfo.getEndOffset(); + if (originalJsDocStart < 0 || originalJsDocEnd < 0) return; - while (jsDocMatcher.find()) { - int end = jsDocMatcher.end(); - // Check if this JSDoc is right before the method (within 200 chars, allowing for whitespace/function keywords) - if (end <= methodStart && methodStart - end < 200) { - // Check if there's no other code between JSDoc and method - // For JavaScript: allow "function", "async", "var/let/const name = ", etc. - String between = text.substring(end, methodStart).trim(); - // Allow empty, or typical JS function declaration patterns - if (between.isEmpty() || - between.matches("^(?:async\\s+)?(?:function\\s+\\w*)?\\s*$") || - between.matches("^(?:var|let|const)\\s+\\w+\\s*=\\s*(?:async\\s+)?(?:function)?\\s*$") || - between.matches("^\\w+\\s*[=:]\\s*(?:async\\s+)?(?:function)?\\s*$") || - between.matches("^(?:public|private|protected|static|final|abstract|synchronized|native|transient|volatile|\\s)+$")) { - jsDocStart = jsDocMatcher.start(); - jsDocContent = jsDocMatcher.group(0); - } + // Calculate how much positions have shifted due to renames before the JSDoc + // Count occurrences that are currently in the text before the original JSDoc start + int shiftBeforeJsDoc = 0; + int lengthDiff = paramName.length() - initialWord.length(); + + // The occurrences we've already found (code occurrences) - count how many are before the JSDoc + for (int[] occ : allOccurrences) { + // Map current position back to approximate original position + int approxOriginalPos = occ[0] - shiftBeforeJsDoc; + if (approxOriginalPos < originalJsDocStart) { + shiftBeforeJsDoc += lengthDiff; } } - if (jsDocContent == null) return; + // Now search for the @param pattern in the current text around where the JSDoc should be + int adjustedJsDocStart = originalJsDocStart + shiftBeforeJsDoc; + int adjustedJsDocEnd = originalJsDocEnd + shiftBeforeJsDoc; - // Find @param paramName in the JSDoc - // Pattern: @param (optional {Type}) paramName - Pattern paramPattern = Pattern.compile("@param\\s+(?:\\{[^}]*\\}\\s+)?(" + Pattern.quote(paramName) + ")(?:\\s|$|-)"); - Matcher paramMatcher = paramPattern.matcher(jsDocContent); + // Clamp to text bounds + adjustedJsDocStart = Math.max(0, adjustedJsDocStart); + adjustedJsDocEnd = Math.min(text.length(), adjustedJsDocEnd + 50); // Add some buffer for expanded text - while (paramMatcher.find()) { - int paramNameStart = jsDocStart + paramMatcher.start(1); - int paramNameEnd = jsDocStart + paramMatcher.end(1); + if (adjustedJsDocStart >= adjustedJsDocEnd) return; + + String jsDocRegion = text.substring(adjustedJsDocStart, adjustedJsDocEnd); + + // Search for @param {Type} paramName or @param paramName pattern + Pattern pattern = Pattern.compile("@param\\s+(?:\\{[^}]*\\}\\s+)?(" + Pattern.quote(paramName) + ")(?:\\s|$|-)"); + Matcher m = pattern.matcher(jsDocRegion); + + while (m.find()) { + int paramNameStart = adjustedJsDocStart + m.start(1); + int paramNameEnd = adjustedJsDocStart + m.end(1); allOccurrences.add(new int[]{paramNameStart, paramNameEnd}); } } From bdda92cae0a455db36c952e8b8ba27d0d90f1db7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 6 Jan 2026 02:53:44 +0200 Subject: [PATCH 225/337] Fixed type aliases and nested interfaces not being properly read from the .d.ts files --- .../js_parser/TypeScriptDefinitionParser.java | 84 +++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) 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 446f826bf..1d47e37fe 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 @@ -16,6 +16,10 @@ public class TypeScriptDefinitionParser { private static final Pattern INTERFACE_PATTERN = Pattern.compile( "export\\s+interface\\s+(\\w+)(?:<[^>]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + // Pattern for nested interfaces without export keyword + private static final Pattern NESTED_INTERFACE_PATTERN = Pattern.compile( + "(?]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + // Similar pattern for classes private static final Pattern CLASS_PATTERN = Pattern.compile( "export\\s+class\\s+(\\w+)(?:<[^>]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); @@ -23,8 +27,9 @@ public class TypeScriptDefinitionParser { private static final Pattern NAMESPACE_PATTERN = Pattern.compile( "export\\s+namespace\\s+(\\w+)\\s*\\{"); + // Make semicolon optional for type aliases (TypeScript doesn't require them) private static final Pattern TYPE_ALIAS_PATTERN = Pattern.compile( - "export\\s+type\\s+(\\w+)\\s*=\\s*([^;]+);"); + "export\\s+type\\s+(\\w+)\\s*=\\s*([^;\\n]+);?"); private static final Pattern METHOD_PATTERN = Pattern.compile( "^\\s*(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*([^;]+);", Pattern.MULTILINE); @@ -156,7 +161,7 @@ private void parseIndexFile(String content) { * Parse interface and class definitions from content. */ private void parseInterfaceFile(String content, String parentNamespace) { - // Find interfaces + // Find exported interfaces Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(content); while (interfaceMatcher.find()) { String interfaceName = interfaceMatcher.group(1); @@ -181,6 +186,39 @@ private void parseInterfaceFile(String content, String parentNamespace) { // Find the body of this interface int bodyStart = interfaceMatcher.end(); int bodyEnd = findMatchingBrace(content, bodyStart - 1); + if (bodyEnd > bodyStart) { + String body = content.substring(bodyStart, bodyEnd); + parseInterfaceBody(body, typeInfo); + + // Parse nested interfaces and type aliases within this interface body + String fullNamespace = parentNamespace != null ? + parentNamespace + "." + interfaceName : interfaceName; + parseNestedTypes(body, fullNamespace); + } + + registry.registerType(typeInfo); + } + + // Find nested interfaces (without export keyword) + Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content); + while (nestedInterfaceMatcher.find()) { + String interfaceName = nestedInterfaceMatcher.group(1); + String extendsClause = nestedInterfaceMatcher.group(2); + + JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace); + if (extendsClause != null) { + String extendsType = extendsClause.trim(); + extendsType = extendsType.replaceAll("<[^>]*>", ""); + if (extendsType.contains(",")) { + extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim(); + } + extendsType = cleanType(extendsType); + typeInfo.setExtends(extendsType); + } + + // Find the body of this nested interface + int bodyStart = nestedInterfaceMatcher.end(); + int bodyEnd = findMatchingBrace(content, bodyStart - 1); if (bodyEnd > bodyStart) { String body = content.substring(bodyStart, bodyEnd); parseInterfaceBody(body, typeInfo); @@ -228,13 +266,11 @@ private void parseInterfaceFile(String content, String parentNamespace) { if (bodyEnd > bodyStart) { String body = content.substring(bodyStart, bodyEnd); - // Recursively parse inner types with namespace prefix + // Parse inner types (both exported and nested) with namespace prefix String fullNamespace = parentNamespace != null ? parentNamespace + "." + namespaceName : namespaceName; parseInterfaceFile(body, fullNamespace); - - // Also handle type aliases within namespace - parseTypeAliases(body, fullNamespace); + parseNestedTypes(body, fullNamespace); } } @@ -244,6 +280,42 @@ private void parseInterfaceFile(String content, String parentNamespace) { } } + /** + * Parse nested types (interfaces and type aliases) within a parent type or namespace. + */ + private void parseNestedTypes(String content, String namespace) { + // Parse nested interfaces + Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content); + while (nestedInterfaceMatcher.find()) { + String interfaceName = nestedInterfaceMatcher.group(1); + String extendsClause = nestedInterfaceMatcher.group(2); + + JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, namespace); + if (extendsClause != null) { + String extendsType = extendsClause.trim(); + extendsType = extendsType.replaceAll("<[^>]*>", ""); + if (extendsType.contains(",")) { + extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim(); + } + extendsType = cleanType(extendsType); + typeInfo.setExtends(extendsType); + } + + // Find the body of this nested interface + int bodyStart = nestedInterfaceMatcher.end(); + int bodyEnd = findMatchingBrace(content, bodyStart - 1); + if (bodyEnd > bodyStart) { + String body = content.substring(bodyStart, bodyEnd); + parseInterfaceBody(body, typeInfo); + } + + registry.registerType(typeInfo); + } + + // Parse type aliases within this context + parseTypeAliases(content, namespace); + } + /** * Parse type aliases (export type X = Y). */ From c10fa8e17ea71a3dc3e7ac75160b5affcfec31d3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 6 Jan 2026 04:01:19 +0200 Subject: [PATCH 226/337] Added global engine objects to JSTypeRegistry and marked them properly as global fields --- .../script/interpreter/ScriptDocument.java | 33 ++++++- .../interpreter/js_parser/JSTypeRegistry.java | 93 +++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) 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 7d72fd632..a96c0195c 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 @@ -1787,11 +1787,11 @@ private List buildMarks() { // Chained field accesses - UNIFIED (uses resolveVariable which handles both) markChainedFieldAccesses(marks); - + markImportedClassUsages(marks); + // Java-specific final passes if (!isJavaScript()) { markCastTypes(marks); - markImportedClassUsages(marks); markUnusedImports(marks); } @@ -4338,6 +4338,21 @@ private void markVariables(List marks) { } } + if (isJavaScript()) { + // check if a variable in global objects + JSTypeRegistry registry = JSTypeRegistry.getInstance(); + String globalObjectType = registry.getGlobalObjectType(name); + if (globalObjectType != null) { + // Resolve the type and create a FieldInfo for it + FieldInfo fieldInfo = resolveVariable(name, position); + if (fieldInfo != null && fieldInfo.isResolved()) { + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); + continue; + } + } + } + // Skip uppercase if not a known field - type handling will deal with it if (isUppercase) continue; @@ -5259,6 +5274,20 @@ FieldInfo resolveVariable(String name, int position) { if (globalFields.containsKey(name)) { return globalFields.get(name); } + + // Check JS global objects from JSTypeRegistry (like API, DBCAPI) + if (isJavaScript()) { + JSTypeRegistry registry = JSTypeRegistry.getInstance(); + String globalObjectType = registry.getGlobalObjectType(name); + if (globalObjectType != null) { + // Resolve the type and create a FieldInfo for it + TypeInfo typeInfo = resolveType(globalObjectType); + if (typeInfo != null && typeInfo.isResolved()) { + // Create a global field for this object with GLOBAL scope + return FieldInfo.globalField(name, typeInfo, -1); + } + } + } return null; } 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 77888054e..f0126243b 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 @@ -2,6 +2,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.util.ResourceLocation; +import noppes.npcs.scripted.NpcAPI; import java.io.*; import java.net.URL; @@ -29,6 +30,10 @@ public class JSTypeRegistry { // Hook function signatures: functionName -> list of (paramName, paramType) pairs // Multiple entries for overloaded hooks private final Map> hooks = new LinkedHashMap<>(); + + // Global object instances: name -> type (e.g., "API" -> "AbstractNpcAPI") + // These are treated as instance objects, not static classes + private final Map globalEngineObjects = new LinkedHashMap<>(); // Primitive types private static final Set PRIMITIVES = new HashSet<>(Arrays.asList( @@ -79,6 +84,8 @@ public void initializeFromResources() { } resolveInheritance(); + registerEngineGlobalObjects(); + initialized = true; System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from resources"); } catch (Exception e) { @@ -318,6 +325,12 @@ private JSTypeInfo getType(String name, Set visited) { } } + // Check global engine objects + if (globalEngineObjects.containsKey(baseName)) { + String typeName = globalEngineObjects.get(baseName); + return getType(typeName, visited); + } + return null; } @@ -399,6 +412,85 @@ public Set getHookNames() { public Map> getAllHooks() { return hooks; } + + /** + * Register a global object instance (like API, DBCAPI from NpcAPI.engineObjects). + * These are treated as instance objects, not static classes. + * @param name The global variable name (e.g., "API") + * @param typeName The type name (e.g., "AbstractNpcAPI") + */ + public void registerGlobalObject(String name, String typeName) { + globalEngineObjects.put(name, typeName); + } + + /** + * Get the type name for a global object. + * @param name The global variable name + * @return The type name, or null if not a registered global object + */ + public String getGlobalObjectType(String name) { + return globalEngineObjects.get(name); + } + + /** + * Check if a name is a registered global object instance. + */ + public boolean isGlobalObject(String name) { + return globalEngineObjects.containsKey(name); + } + + /** + * Get all registered global objects. + */ + public Map getGlobalEngineObjects() { + return Collections.unmodifiableMap(globalEngineObjects); + } + + /** + * Registers global objects from NpcAPI.engineObjects into JSTypeRegistry. + * This allows the IDE to recognize API, DBCAPI, etc. as instance objects with autocomplete. + */ + private void registerEngineGlobalObjects() { + Map engineObjects = new HashMap<>(NpcAPI.engineObjects); + engineObjects.put("API", NpcAPI.Instance()); //default API object + + if (engineObjects != null) { + for (Map.Entry entry : engineObjects.entrySet()) { + String name = entry.getKey(); // e.g., "API", "DBCAPI" + Object obj = entry.getValue(); // e.g., AbstractNpcAPI instance + + if (obj != null) { + // Get the actual class name + String concreteClassName = obj.getClass().getSimpleName(); + + // Map concrete implementation classes to their abstract interface names + // (because .d.ts files define the abstract interfaces, not the implementations) + String typeName = mapConcreteToAbstractClassName(concreteClassName); + + // Register as global object (instance, not static) + registerGlobalObject(name, typeName); + } + } + } + } + + /** + * Maps concrete implementation class names to their abstract interface names. + * For example: "NpcAPI" -> "AbstractNpcAPI", "DBCAPI" -> "AbstractDBCAPI" + */ + private String mapConcreteToAbstractClassName(String concreteClassName) { + // Check if the type exists in the registry as-is + if (types.containsKey(concreteClassName)) + return concreteClassName; + + // Try prepending "Abstract" if it doesn't exist + String abstractName = "Abstract" + concreteClassName; + if (types.containsKey(abstractName)) + return abstractName; + + // Default: return the original name + return concreteClassName; + } /** * Check if initialized. @@ -414,6 +506,7 @@ public void clear() { types.clear(); typeAliases.clear(); hooks.clear(); + globalEngineObjects.clear(); initialized = false; } From beefd35bd909a95c3528b7d1e462684d5dd2e988 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 6 Jan 2026 04:09:25 +0200 Subject: [PATCH 227/337] actually nvm --- .../util/script/interpreter/js_parser/JSTypeRegistry.java | 7 ------- 1 file changed, 7 deletions(-) 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 f0126243b..8dfec22cf 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 @@ -324,13 +324,6 @@ private JSTypeInfo getType(String name, Set visited) { return type; } } - - // Check global engine objects - if (globalEngineObjects.containsKey(baseName)) { - String typeName = globalEngineObjects.get(baseName); - return getType(typeName, visited); - } - return null; } From 35c2a08228df9a2f8c4a142752737aeceaec6b58 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 6 Jan 2026 04:15:57 +0200 Subject: [PATCH 228/337] Added AbstractDBCAPI.d.ts (DBCAPI fully functional) --- .../kamkeel/npcdbc/api/AbstractDBCAPI.d.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts new file mode 100644 index 000000000..08807e0bc --- /dev/null +++ b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts @@ -0,0 +1,45 @@ +/** + * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 + * Package: kamkeel.npcdbc.api + */ + +export interface AbstractDBCAPI { + + // Methods + getFormHandler(): import('./form/IFormHandler').IFormHandler; + getAuraHandler(): import('./aura/IAuraHandler').IAuraHandler; + getOutlineHandler(): import('./outline/IOutlineHandler').IOutlineHandler; + getBonusHandler(): import('./effect/IBonusHandler').IBonusHandler; + getDBCEffectHandler(): import('./effect/IDBCEffectHandler').IDBCEffectHandler; + createForm(name: string): import('./form/IForm').IForm; + createAura(name: string): import('./aura/IAura').IAura; + getAura(name: string): import('./aura/IAura').IAura; + getOrCreateForm(name: string): import('./form/IForm').IForm; + getForm(name: string): import('./form/IForm').IForm; + createOutline(name: string): import('./outline/IOutline').IOutline; + getOutline(name: string): import('./outline/IOutline').IOutline; + forceDodge(dodger: import('../../../noppes/npcs/api/entity/IEntity').IEntity, attacker: import('../../../noppes/npcs/api/entity/IEntity').IEntity): void; + abstractDBCData(): import('./npc/IDBCStats').IDBCStats; + getDBCData(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc): import('./npc/IDBCStats').IDBCStats; + getDBCDisplay(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc): import('./npc/IDBCDisplay').IDBCDisplay; + doDBCDamage(player: import('../../../noppes/npcs/api/entity/IPlayer').IPlayer, stats: import('./npc/IDBCStats').IDBCStats, damage: number): void; + getRaceName(race: number): string; + getFormName(race: number, form: number): string; + getAllFormMasteryData(raceid: number, formId: number): string[]; + getAllFormsLength(race: number, nonRacial: boolean): number; + getAllForms(race: number, nonRacial: boolean): string[]; + createKiAttack(): import('./IKiAttack').IKiAttack; + createKiAttack(type: number, speed: number, damage: number, hasEffect: boolean, color: number, density: number, hasSound: boolean, chargePercent: number): import('./IKiAttack').IKiAttack; + fireKiAttack(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc, type: number, speed: number, damage: number, hasEffect: boolean, color: number, density: number, hasSound: boolean, chargePercent: number): void; + fireKiAttack(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc, kiAttack: import('./IKiAttack').IKiAttack): void; + getSkillTPCostSingle(skillName: string, level: number): number; + getSkillMindCostSingle(skillName: string, level: number): number; + getSkillMindCostRecursive(skillName: string, level: number): number; + getSkillTPCostRecursive(skillName: string, level: number): number; + getSkillRacialTPCostSingle(race: number, level: number): number; + getSkillRacialTPMindSingle(race: number, level: number): number; + getSkillRacialTPCostSingleRecursive(race: number, level: number): number; + getSkillRacialTPMindSingleRecursive(race: number, level: number): number; + getUltraInstinctMaxLevel(): number; + +} From e883b77117ce874e0cb052e9349b21e9ad10e263 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 6 Jan 2026 04:16:08 +0200 Subject: [PATCH 229/337] Improved AbstractNpcApi.d.ts --- .../api/noppes/npcs/api/AbstractNpcAPI.d.ts | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/AbstractNpcAPI.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/AbstractNpcAPI.d.ts index 27e1b3e22..921d74b9c 100644 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/AbstractNpcAPI.d.ts +++ b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/AbstractNpcAPI.d.ts @@ -5,14 +5,9 @@ export class AbstractNpcAPI { - // Fields - instance: import('./AbstractNpcAPI').AbstractNpcAPI; - instance: any; - null: any; - c: any; - instance: any; - // Methods + Instance(): AbstractNpcAPI; + IsAvailable(): boolean; getTempData(key: string): any; setTempData(key: string, value: any): void; hasTempData(key: string): boolean; @@ -30,14 +25,14 @@ export class AbstractNpcAPI { addGlobalObject(key: string, obj: any): void; removeGlobalObject(key: string): void; hasGlobalObject(key: string): boolean; - getEngineObjects(): object; sizeOfObject(obj: any): number; stopServer(): void; getCurrentPlayerCount(): number; getMaxPlayers(): number; kickAllPlayers(): void; isHardcore(): boolean; - getFile(path: string): File; + getFile(path: string): import('../../../java/io/File').File; getServerOwner(): string; getFactions(): import('./handler/IFactionHandler').IFactionHandler; getRecipes(): import('./handler/IRecipeHandler').IRecipeHandler; @@ -52,43 +47,45 @@ export class AbstractNpcAPI { getLocations(): import('./handler/ITransportHandler').ITransportHandler; getAnimations(): import('./handler/IAnimationHandler').IAnimationHandler; getAllBiomeNames(): string[]; - getChunkLoadingNPCs(): INpc[]; - getLoadedEntities(): []; + createNPC(world: import('./IWorld').IWorld): import('./entity/ICustomNpc').ICustomNpc; + spawnNPC(world: import('./IWorld').IWorld, x: number, y: number, z: number): import('./entity/ICustomNpc').ICustomNpc; + spawnNPC(world: import('./IWorld').IWorld, pos: import('./IPos').IPos): import('./entity/ICustomNpc').ICustomNpc; + getIEntity(entity: any): import('./entity/IEntity').IEntity; + getPlayer(username: string): import('./entity/IPlayer').IPlayer; + getChunkLoadingNPCs(): import('./entity/ICustomNpc').ICustomNpc[]; + getLoadedEntities(): import('./entity/IEntity').IEntity[]; getIBlock(world: import('./IWorld').IWorld, x: number, y: number, z: number): import('./IBlock').IBlock; getIBlock(world: import('./IWorld').IWorld, pos: import('./IPos').IPos): import('./IBlock').IBlock; getITileEntity(world: import('./IWorld').IWorld, pos: import('./IPos').IPos): import('./ITileEntity').ITileEntity; getITileEntity(world: import('./IWorld').IWorld, x: number, y: number, z: number): import('./ITileEntity').ITileEntity; - getITileEntity(tileEntity: TileEntity): import('./ITileEntity').ITileEntity; - getIPos(pos: import('../../../net/minecraft/util/math/BlockPos').BlockPos): import('./IPos').IPos; - getIPos(x: number, y: number, z: number): import('./IPos').IPos; - getIPos(x: number, y: number, z: number): import('./IPos').IPos; + getITileEntity(tileEntity: any): import('./ITileEntity').ITileEntity; + getIPos(pos: any): import('./IPos').IPos; getIPos(x: number, y: number, z: number): import('./IPos').IPos; getIPos(serializedPos: number): import('./IPos').IPos; getAllInBox(from: import('./IPos').IPos, to: import('./IPos').IPos, sortByDistance: boolean): import('./IPos').IPos[]; getAllInBox(from: import('./IPos').IPos, to: import('./IPos').IPos): import('./IPos').IPos[]; - getIContainer(var1: IInventory): import('./IContainer').IContainer; - getIContainer(var1: Container): import('./IContainer').IContainer; - getIItemStack(var1: ItemStack): import('./item/IItemStack').IItemStack; - getIWorld(var1: World): import('./IWorld').IWorld; - getIWorld(var1: number): import('./IWorld').IWorld; - getIWorldLoad(var1: number): import('./IWorld').IWorld; + getIContainer(inventory: any): import('./IContainer').IContainer; + getIItemStack(itemStack: any): import('./item/IItemStack').IItemStack; + getIWorld(world: any): import('./IWorld').IWorld; + getIWorld(dimensionId: number): import('./IWorld').IWorld; + getIWorldLoad(dimensionId: number): import('./IWorld').IWorld; getActionManager(): import('./handler/IActionManager').IActionManager; getIWorlds(): import('./IWorld').IWorld[]; - getIDamageSource(var1: DamageSource): import('./IDamageSource').IDamageSource; - getIDamageSource(entity: IEntity Date: Tue, 6 Jan 2026 22:05:40 +0200 Subject: [PATCH 230/337] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 23d9047f1..2881c886c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ .project /bin/ /config/ +/copilot/ /crash-reports/ /logs/ options.txt From f0ec2b040186e88f8b664ff40d288daca6cebaf0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 01:07:17 +0200 Subject: [PATCH 231/337] removed tags --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2881c886c..5708af3ec 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /bin/ /config/ /copilot/ +/buildSrc/build/ /crash-reports/ /logs/ options.txt From 274e96af03cbb6348c79ff6f77237c23c81e0482 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 17:30:17 +0200 Subject: [PATCH 232/337] generateTypeScriptDefinitions gradle task --- build.gradle | 16 ++++++++++++++-- settings.gradle | 10 +++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b76ad9d0c..571c4cecc 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ plugins { id 'com.gtnewhorizons.gtnhconvention' id 'idea' + id "dts.typescript-generator" } version = "1.10.3" @@ -41,5 +42,16 @@ sourceSets.main.java { srcDirs += apiDir } -// Modify the existing 'build' task to depend on 'updateAPI' -//tasks.apiClasses.dependsOn 'updateAPI' +// ============================================================================ +// TypeScript Definition Generation Task +// Generates .d.ts files from Java API sources for scripting IDE support +// ============================================================================ +// TypeScript plugin is applied above in the main plugins block + +tasks.named("generateTypeScriptDefinitions").configure { + sourceDirectories = ['src/api/java'] + outputDirectory = "src/main/resources/assets/${project.property("modId")}/api" + apiPackages = ['noppes.npcs.api'] as Set + cleanOutputFirst = true // Clean old generated files before regenerating +} + diff --git a/settings.gradle b/settings.gradle index ec9681d97..791ed6664 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,14 @@ - pluginManagement { + resolutionStrategy { + eachPlugin { + if(requested.id.toString() == "dts.typescript-generator") { + useModule("com.github.bigguy345:dts-gradle-plugin:95b8ab4") + } + } + } + repositories { + maven { url "https://jitpack.io"} maven { // RetroFuturaGradle name "GTNH Maven" From df0cc666d4bcac79862ef21f11a64b530207667f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 18:03:17 +0200 Subject: [PATCH 233/337] Removed DBC Addon .d.ts files --- .../kamkeel/npcdbc/api/AbstractDBCAPI.d.ts | 45 ----- .../api/kamkeel/npcdbc/api/IDBCAddon.d.ts | 162 ------------------ .../api/kamkeel/npcdbc/api/IKiAttack.d.ts | 22 --- .../api/kamkeel/npcdbc/api/aura/IAura.d.ts | 19 -- .../kamkeel/npcdbc/api/event/IDBCEvent.d.ts | 49 ------ .../api/kamkeel/npcdbc/api/form/IForm.d.ts | 55 ------ .../kamkeel/npcdbc/api/outline/IOutline.d.ts | 18 -- 7 files changed, 370 deletions(-) delete mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts deleted file mode 100644 index 08807e0bc..000000000 --- a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/AbstractDBCAPI.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: kamkeel.npcdbc.api - */ - -export interface AbstractDBCAPI { - - // Methods - getFormHandler(): import('./form/IFormHandler').IFormHandler; - getAuraHandler(): import('./aura/IAuraHandler').IAuraHandler; - getOutlineHandler(): import('./outline/IOutlineHandler').IOutlineHandler; - getBonusHandler(): import('./effect/IBonusHandler').IBonusHandler; - getDBCEffectHandler(): import('./effect/IDBCEffectHandler').IDBCEffectHandler; - createForm(name: string): import('./form/IForm').IForm; - createAura(name: string): import('./aura/IAura').IAura; - getAura(name: string): import('./aura/IAura').IAura; - getOrCreateForm(name: string): import('./form/IForm').IForm; - getForm(name: string): import('./form/IForm').IForm; - createOutline(name: string): import('./outline/IOutline').IOutline; - getOutline(name: string): import('./outline/IOutline').IOutline; - forceDodge(dodger: import('../../../noppes/npcs/api/entity/IEntity').IEntity, attacker: import('../../../noppes/npcs/api/entity/IEntity').IEntity): void; - abstractDBCData(): import('./npc/IDBCStats').IDBCStats; - getDBCData(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc): import('./npc/IDBCStats').IDBCStats; - getDBCDisplay(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc): import('./npc/IDBCDisplay').IDBCDisplay; - doDBCDamage(player: import('../../../noppes/npcs/api/entity/IPlayer').IPlayer, stats: import('./npc/IDBCStats').IDBCStats, damage: number): void; - getRaceName(race: number): string; - getFormName(race: number, form: number): string; - getAllFormMasteryData(raceid: number, formId: number): string[]; - getAllFormsLength(race: number, nonRacial: boolean): number; - getAllForms(race: number, nonRacial: boolean): string[]; - createKiAttack(): import('./IKiAttack').IKiAttack; - createKiAttack(type: number, speed: number, damage: number, hasEffect: boolean, color: number, density: number, hasSound: boolean, chargePercent: number): import('./IKiAttack').IKiAttack; - fireKiAttack(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc, type: number, speed: number, damage: number, hasEffect: boolean, color: number, density: number, hasSound: boolean, chargePercent: number): void; - fireKiAttack(npc: import('../../../noppes/npcs/api/entity/ICustomNpc').ICustomNpc, kiAttack: import('./IKiAttack').IKiAttack): void; - getSkillTPCostSingle(skillName: string, level: number): number; - getSkillMindCostSingle(skillName: string, level: number): number; - getSkillMindCostRecursive(skillName: string, level: number): number; - getSkillTPCostRecursive(skillName: string, level: number): number; - getSkillRacialTPCostSingle(race: number, level: number): number; - getSkillRacialTPMindSingle(race: number, level: number): number; - getSkillRacialTPCostSingleRecursive(race: number, level: number): number; - getSkillRacialTPMindSingleRecursive(race: number, level: number): number; - getUltraInstinctMaxLevel(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts deleted file mode 100644 index 58f98ec62..000000000 --- a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IDBCAddon.d.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ DBC Addon - * Package: kamkeel.npcdbc.api - */ - -import { IDBCPlayer } from '../../../noppes/npcs/api/entity/IDBCPlayer'; -import { IEntityLivingBase } from '../../../noppes/npcs/api/entity/IEntityLivingBase'; -import { IForm } from './form/IForm'; -import { IAura } from './aura/IAura'; -import { IOutline } from './outline/IOutline'; -import { IKiAttack } from './IKiAttack'; -import { IPlayer } from '../../../noppes/npcs/api/entity/IPlayer'; - -export interface IDBCAddon extends IDBCPlayer { - getAllFullAttributes(): number[]; - getUsedMind(): number; - getAvailableMind(): number; - setLockOnTarget(lockOnTarget: IEntityLivingBase): void; - setKiFistOn(on: boolean): void; - setKiProtectionOn(on: boolean): void; - setKiWeaponType(type: number): void; - kiFistOn(): boolean; - kiProtectionOn(): boolean; - getKiWeaponType(): number; - isTurboOn(): boolean; - setTurboState(on: boolean): void; - getMaxBody(): number; - getMaxHP(): number; - getBodyPercentage(): number; - getMaxKi(): number; - getMaxStamina(): number; - getAllAttributes(): number[]; - modifyAllAttributes(attri: number[], multiplyAddedAttris: boolean, multiValue: number): void; - modifyAllAttributes(Num: number, setStatsToNum: boolean): void; - modifyAllAttributes(submitted: number[], setStats: boolean): void; - multiplyAttribute(statid: number, multi: number): void; - multiplyAllAttributes(multi: number): void; - getFullAttribute(statid: number): number; - getRaceName(): string; - getCurrentDBCFormName(): string; - changeDBCMastery(formName: string, amount: number, add: boolean): void; - getDBCMasteryValue(formName: string): number; - getAllDBCMasteries(): string; - isDBCFusionSpectator(): boolean; - isChargingKi(): boolean; - getSkillLevel(skillname: string): number; - getMaxStat(attribute: number): number; - getCurrentStat(attribute: number): number; - getCurrentFormMultiplier(): number; - getMajinAbsorptionRace(): number; - setMajinAbsorptionRace(race: number): void; - getMajinAbsorptionPower(): number; - setMajinAbsorptionPower(power: number): void; - isMUI(): boolean; - isKO(): boolean; - isUI(): boolean; - isMystic(): boolean; - isKaioken(): boolean; - isGOD(): boolean; - isLegendary(): boolean; - isDivine(): boolean; - isMajin(): boolean; - - // Custom Form Setters (Grouped) - setCustomForm(formID: number): void; - setCustomForm(formID: number, ignoreUnlockCheck: boolean): void; - setCustomForm(form: IForm): void; - setCustomForm(form: IForm, ignoreUnlockCheck: boolean): void; - setCustomForm(formName: string): void; - setCustomForm(formName: string, ignoreUnlockCheck: boolean): void; - - // Custom Form Management (Grouped) - hasCustomForm(formName: string): boolean; - hasCustomForm(formID: number): boolean; - getCustomForms(): IForm[]; - giveCustomForm(formName: string): void; - giveCustomForm(form: IForm): void; - removeCustomForm(formName: string): void; - removeCustomForm(formName: string, removesMastery: boolean): void; - removeCustomForm(form: IForm): void; - removeCustomForm(form: IForm, removesMastery: boolean): void; - - // Flight - setFlight(flightOn: boolean): void; - isFlying(): boolean; - setAllowFlight(allowFlight: boolean): void; - setFlightSpeedRelease(release: number): void; - setBaseFlightSpeed(speed: number): void; - setDynamicFlightSpeed(speed: number): void; - setFlightGravity(isEffected: boolean): void; - setFlightDefaults(): void; - setSprintSpeed(speed: number): void; - - // Selected Form - getSelectedForm(): IForm; - setSelectedForm(form: IForm): void; - setSelectedForm(formID: number): void; - removeSelectedForm(): void; - getSelectedDBCForm(): number; - setSelectedDBCForm(formID: number): void; - removeSelectedDBCForm(): void; - getCurrentForm(): IForm; - isInCustomForm(): boolean; - isInCustomForm(form: IForm): boolean; - isInCustomForm(formID: number): boolean; - - // Aura - setAuraSelection(auraName: string): void; - setAuraSelection(aura: IAura): void; - setAuraSelection(auraID: number): void; - giveAura(auraName: string): void; - giveAura(aura: IAura): void; - giveAura(auraID: number): void; - hasAura(auraName: string): boolean; - hasAura(auraId: number): boolean; - removeAura(auraName: string): void; - removeAura(aura: IAura): void; - removeAura(auraID: number): void; - setAura(auraName: string): void; - setAura(aura: IAura): void; - setAura(auraID: number): void; - removeCurrentAura(): void; - removeAuraSelection(): void; - isInAura(): boolean; - isInAura(aura: IAura): boolean; - isInAura(auraName: string): boolean; - isInAura(auraID: number): boolean; - - // Mastery - setCustomMastery(formID: number, value: number): void; - setCustomMastery(formID: number, value: number, ignoreUnlockCheck: boolean): void; - setCustomMastery(form: IForm, value: number): void; - setCustomMastery(form: IForm, value: number, ignoreUnlockCheck: boolean): void; - addCustomMastery(formID: number, value: number): void; - addCustomMastery(formID: number, value: number, ignoreUnlockCheck: boolean): void; - addCustomMastery(form: IForm, value: number): void; - addCustomMastery(form: IForm, value: number, ignoreUnlockCheck: boolean): void; - getCustomMastery(formID: number): number; - getCustomMastery(formID: number, checkFusion: boolean): number; - getCustomMastery(form: IForm): number; - getCustomMastery(form: IForm, checkFusion: boolean): number; - removeCustomMastery(formID: number): void; - removeCustomMastery(form: IForm): void; - - // Outline - removeOutline(): void; - setOutline(outline: IOutline): void; - setOutline(outlineName: string): void; - setOutline(outlineID: number): void; - getOutline(): IOutline; - - // Other - getFusionPartner(): IPlayer; - fireKiAttack(type: number, speed: number, damage: number, hasEffect: boolean, color: number, density: number, hasSound: boolean, chargePercent: number): void; - fireKiAttack(kiAttack: IKiAttack): void; - isReleasing(): boolean; - isMeditating(): boolean; - isSuperRegen(): boolean; - isSwooping(): boolean; - isInMedicalLiquid(): boolean; - getAttackFromSlot(slot: number): IKiAttack; -} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts deleted file mode 100644 index 1e7503cb5..000000000 --- a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/IKiAttack.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface IKiAttack { - getType(): number; - setType(type: number): void; - getSpeed(): number; - setSpeed(speed: number): void; - getDamage(): number; - setDamage(damage: number): void; - hasEffect(): boolean; - setHasEffect(hasEffect: boolean): void; - getColor(): number; - setColor(color: number): void; - getDensity(): number; - setDensity(density: number): void; - hasSound(): boolean; - setHasSound(hasSound: boolean): void; - getChargePercent(): number; - setChargePercent(chargePercent: number): void; - respectFormDestoryerConfig(): boolean; - setRespectFormDestroyerConfig(respectFormConfig: boolean): void; - isDestroyerAttack(): boolean; - setDestroyerAttack(isDestroyer: boolean): void; -} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts deleted file mode 100644 index 66968ff0b..000000000 --- a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/aura/IAura.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface IAura { - getName(): string; - setName(name: string): void; - getMenuName(): string; - setMenuName(name: string): void; - getID(): number; - setID(newID: number): void; - assignToPlayer(player: IPlayer): void; - removeFromPlayer(player: IPlayer): void; - assignToPlayer(playerName: string): void; - removeFromPlayer(playerName: string): void; - getDisplay(): any; // IAuraDisplay - getSecondaryAuraID(): number; - getSecondaryAura(): IAura; - setSecondaryAura(id: number): void; - setSecondaryAura(aura: IAura): void; - clone(): IAura; - save(): IAura; -} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts deleted file mode 100644 index 02347f3eb..000000000 --- a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/event/IDBCEvent.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ DBC Addon - * Package: kamkeel.npcdbc.api.event - */ - -import { IPlayerEvent } from '../../../noppes/npcs/api/event/IPlayerEvent'; -import { IDamageSource } from '../../../noppes/npcs/api/IDamageSource'; - -export type IDBCEvent = IPlayerEvent - -export namespace IDBCEvent { - export interface CapsuleUsedEvent extends IDBCEvent { - getType(): number; - getSubType(): number; - getCapsuleName(): string; - } - - export type SenzuUsedEvent = IDBCEvent - - export interface FormChangeEvent extends IDBCEvent { - getFormBeforeID(): number; - getFormAfterID(): number; - isFormBeforeCustom(): boolean; - isFormAfterCustom(): boolean; - } - - export interface DamagedEvent extends IDBCEvent { - getDamage(): number; - setDamage(damage: number): void; - - getStaminaReduced(): number; - setStaminaReduced(stamina: number): void; - - willKo(): boolean; - - getKiReduced(): number; - setKiReduced(ki: number): void; - - getDamageSource(): IDamageSource; - isDamageSourceKiAttack(): boolean; - getType(): number; - } - - export type DBCReviveEvent = IDBCEvent - - export interface DBCKnockout extends IDBCEvent { - getDamageSource(): IDamageSource; - } -} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts deleted file mode 100644 index e7e5f5926..000000000 --- a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/form/IForm.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -export interface IForm { - getName(): string; - setName(name: string): void; - getMenuName(): string; - setMenuName(name: string): void; - getRace(): number; - setRace(race: number): void; - getAllMulti(): number[]; // float[] - setAllMulti(allMulti: number): void; // float - raceEligible(race: number): boolean; - raceEligible(player: IPlayer): boolean; - setAttributeMulti(id: number, multi: number): void; - getAttributeMulti(id: number): number; - assignToPlayer(player: IPlayer): void; - removeFromPlayer(player: IPlayer): void; - assignToPlayer(playerName: string): void; - removeFromPlayer(playerName: string): void; - removeFromPlayer(player: IPlayer, removesMastery: boolean): void; - removeFromPlayer(playerName: string, removesMastery: boolean): void; - getAscendSound(): string; - setAscendSound(directory: string): void; - getDescendSound(): string; - setDescendSound(directory: string): void; - getID(): number; - setID(newID: number): void; - getChildID(): number; - hasChild(): boolean; - linkChild(formID: number): void; - linkChild(form: IForm): void; - isFromParentOnly(): boolean; - setFromParentOnly(set: boolean): void; - addFormRequirement(race: number, state: number): void; - removeFormRequirement(race: number): void; - getFormRequirement(race: number): number; - isChildOf(parent: IForm): boolean; - getChild(): IForm; - removeChildForm(): void; - getParentID(): number; - hasParent(): boolean; - linkParent(formID: number): void; - linkParent(form: IForm): void; - getParent(): IForm; - getTimer(): number; - setTimer(timeInTicks: number): void; - hasTimer(): boolean; - removeParentForm(): void; - getMastery(): any; // IFormMastery - getDisplay(): any; // IFormDisplay - getStackable(): any; // IFormStackable - getAdvanced(): any; // IFormAdvanced - setMindRequirement(mind: number): void; - getMindRequirement(): number; - clone(): IForm; - save(): IForm; -} diff --git a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts b/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts deleted file mode 100644 index 5e6050320..000000000 --- a/src/main/resources/assets/customnpcs/api/kamkeel/npcdbc/api/outline/IOutline.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface IOutline { - setInnerColor(color: number, alpha: number): void; - setOuterColor(color: number, alpha: number): void; - setSize(size: number): IOutline; - setNoiseSize(size: number): IOutline; - setSpeed(speed: number): IOutline; - setPulsingSpeed(speed: number): IOutline; - setColorSmoothness(smoothness: number): IOutline; - setColorInterpolation(interp: number): IOutline; - getName(): string; - setName(name: string): void; - getMenuName(): string; - setMenuName(name: string): void; - getID(): number; - setID(newID: number): void; - clone(): IOutline; - save(): IOutline; -} From c43359345922aab9f94c4b2e1708948be6132221 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 18:11:52 +0200 Subject: [PATCH 234/337] Added net.minecraft to CNPC+ .d.ts apiPackages --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 571c4cecc..5719aee5a 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ sourceSets.main.java { tasks.named("generateTypeScriptDefinitions").configure { sourceDirectories = ['src/api/java'] outputDirectory = "src/main/resources/assets/${project.property("modId")}/api" - apiPackages = ['noppes.npcs.api'] as Set + apiPackages = ['noppes.npcs.api', 'net.minecraft'] as Set cleanOutputFirst = true // Clean old generated files before regenerating } From 7d6bacb824ad889f53d2c7f308f8c83049d767c0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 20:31:02 +0200 Subject: [PATCH 235/337] Fixed StackOverflows --- .../autocomplete/JSAutocompleteProvider.java | 28 ++++++++---- .../interpreter/js_parser/JSTypeInfo.java | 45 +++++++++++++++++-- .../interpreter/js_parser/JSTypeRegistry.java | 9 ++++ 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index e9217a4d3..c594ecd02 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -101,13 +101,19 @@ protected void addMemberSuggestions(Context context, List item * Recursively add methods from a type and its parents. */ protected void addMethodsFromType(JSTypeInfo type, List items, Set added) { - addMethodsFromType(type, items, added, 0); + addMethodsFromType(type, items, added, 0, new HashSet<>()); } /** - * Recursively add methods from a type and its parents with inheritance depth tracking. + * Recursively add methods from a type and its parents with inheritance depth tracking and cycle detection. */ - private void addMethodsFromType(JSTypeInfo type, List items, Set added, int depth) { + private void addMethodsFromType(JSTypeInfo type, List items, Set added, int depth, Set visited) { + // Prevent infinite recursion from circular inheritance + if (visited.contains(type.getFullName())) { + return; + } + visited.add(type.getFullName()); + for (JSMethodInfo method : type.getMethods().values()) { String name = method.getName(); // Skip overload markers (name$1, name$2, etc.) @@ -122,7 +128,7 @@ private void addMethodsFromType(JSTypeInfo type, List items, S // Add from parent type with incremented depth if (type.getResolvedParent() != null) { - addMethodsFromType(type.getResolvedParent(), items, added, depth + 1); + addMethodsFromType(type.getResolvedParent(), items, added, depth + 1, visited); } } @@ -130,13 +136,19 @@ private void addMethodsFromType(JSTypeInfo type, List items, S * Recursively add fields from a type and its parents. */ protected void addFieldsFromType(JSTypeInfo type, List items, Set added) { - addFieldsFromType(type, items, added, 0); + addFieldsFromType(type, items, added, 0, new HashSet<>()); } /** - * Recursively add fields from a type and its parents with inheritance depth tracking. + * Recursively add fields from a type and its parents with inheritance depth tracking and cycle detection. */ - private void addFieldsFromType(JSTypeInfo type, List items, Set added, int depth) { + private void addFieldsFromType(JSTypeInfo type, List items, Set added, int depth, Set visited) { + // Prevent infinite recursion from circular inheritance + if (visited.contains(type.getFullName())) { + return; + } + visited.add(type.getFullName()); + for (JSFieldInfo field : type.getFields().values()) { if (!added.contains(field.getName())) { added.add(field.getName()); @@ -146,7 +158,7 @@ private void addFieldsFromType(JSTypeInfo type, List items, Se // Add from parent type with incremented depth if (type.getResolvedParent() != null) { - addFieldsFromType(type.getResolvedParent(), items, added, depth + 1); + addFieldsFromType(type.getResolvedParent(), items, added, depth + 1, visited); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index 7efdd8481..416720f37 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -89,12 +89,25 @@ public void setResolvedParent(JSTypeInfo parent) { * Get a method by name, including inherited methods. */ public JSMethodInfo getMethod(String name) { + return getMethod(name, new HashSet<>()); + } + + /** + * Internal method to get method with cycle detection. + */ + private JSMethodInfo getMethod(String name, Set visited) { + // Prevent infinite recursion from circular inheritance + if (visited.contains(this.fullName)) { + return null; + } + visited.add(this.fullName); + JSMethodInfo method = methods.get(name); if (method != null) return method; // Check parent if (resolvedParent != null) { - return resolvedParent.getMethod(name); + return resolvedParent.getMethod(name, visited); } return null; } @@ -103,8 +116,21 @@ public JSMethodInfo getMethod(String name) { * Get all methods with a given name (for overloads). */ public List getMethodOverloads(String name) { + return getMethodOverloads(name, new HashSet<>()); + } + + /** + * Internal method to get method overloads with cycle detection. + */ + private List getMethodOverloads(String name, Set visited) { List overloads = new ArrayList<>(); + // Prevent infinite recursion from circular inheritance + if (visited.contains(this.fullName)) { + return overloads; + } + visited.add(this.fullName); + // Get from this type if (methods.containsKey(name)) { overloads.add(methods.get(name)); @@ -118,7 +144,7 @@ public List getMethodOverloads(String name) { // Get from parent if (resolvedParent != null) { - overloads.addAll(resolvedParent.getMethodOverloads(name)); + overloads.addAll(resolvedParent.getMethodOverloads(name, visited)); } return overloads; @@ -135,12 +161,25 @@ public boolean hasMethod(String name) { * Get a field by name, including inherited fields. */ public JSFieldInfo getField(String name) { + return getField(name, new HashSet<>()); + } + + /** + * Internal method to get field with cycle detection. + */ + private JSFieldInfo getField(String name, Set visited) { + // Prevent infinite recursion from circular inheritance + if (visited.contains(this.fullName)) { + return null; + } + visited.add(this.fullName); + JSFieldInfo field = fields.get(name); if (field != null) return field; // Check parent if (resolvedParent != null) { - return resolvedParent.getField(name); + return resolvedParent.getField(name, visited); } return null; } 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 8dfec22cf..dd497e465 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 @@ -368,7 +368,16 @@ public String getHookParameterType(String functionName) { public void resolveInheritance() { for (JSTypeInfo type : types.values()) { JSTypeInfo child = type; + Set visited = new HashSet<>(); while (child != null && child.getExtendsType() != null && child.getResolvedParent() == null) { + // Prevent circular inheritance + if (visited.contains(child.getFullName())) { + System.out.println("WARNING: Circular inheritance detected: " + child.getFullName() + + " extends " + child.getExtendsType()); + break; + } + visited.add(child.getFullName()); + JSTypeInfo parent = getType(child.getExtendsType()); if (parent != null) { child.setResolvedParent(parent); From 231163481746349d00787a5ec93ead22661510ac Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 22:46:04 +0200 Subject: [PATCH 236/337] Revert "Fixed StackOverflows" This reverts commit 7d6bacb824ad889f53d2c7f308f8c83049d767c0. --- .../autocomplete/JSAutocompleteProvider.java | 28 ++++-------- .../interpreter/js_parser/JSTypeInfo.java | 45 ++----------------- .../interpreter/js_parser/JSTypeRegistry.java | 9 ---- 3 files changed, 11 insertions(+), 71 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index c594ecd02..e9217a4d3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -101,19 +101,13 @@ protected void addMemberSuggestions(Context context, List item * Recursively add methods from a type and its parents. */ protected void addMethodsFromType(JSTypeInfo type, List items, Set added) { - addMethodsFromType(type, items, added, 0, new HashSet<>()); + addMethodsFromType(type, items, added, 0); } /** - * Recursively add methods from a type and its parents with inheritance depth tracking and cycle detection. + * Recursively add methods from a type and its parents with inheritance depth tracking. */ - private void addMethodsFromType(JSTypeInfo type, List items, Set added, int depth, Set visited) { - // Prevent infinite recursion from circular inheritance - if (visited.contains(type.getFullName())) { - return; - } - visited.add(type.getFullName()); - + private void addMethodsFromType(JSTypeInfo type, List items, Set added, int depth) { for (JSMethodInfo method : type.getMethods().values()) { String name = method.getName(); // Skip overload markers (name$1, name$2, etc.) @@ -128,7 +122,7 @@ private void addMethodsFromType(JSTypeInfo type, List items, S // Add from parent type with incremented depth if (type.getResolvedParent() != null) { - addMethodsFromType(type.getResolvedParent(), items, added, depth + 1, visited); + addMethodsFromType(type.getResolvedParent(), items, added, depth + 1); } } @@ -136,19 +130,13 @@ private void addMethodsFromType(JSTypeInfo type, List items, S * Recursively add fields from a type and its parents. */ protected void addFieldsFromType(JSTypeInfo type, List items, Set added) { - addFieldsFromType(type, items, added, 0, new HashSet<>()); + addFieldsFromType(type, items, added, 0); } /** - * Recursively add fields from a type and its parents with inheritance depth tracking and cycle detection. + * Recursively add fields from a type and its parents with inheritance depth tracking. */ - private void addFieldsFromType(JSTypeInfo type, List items, Set added, int depth, Set visited) { - // Prevent infinite recursion from circular inheritance - if (visited.contains(type.getFullName())) { - return; - } - visited.add(type.getFullName()); - + private void addFieldsFromType(JSTypeInfo type, List items, Set added, int depth) { for (JSFieldInfo field : type.getFields().values()) { if (!added.contains(field.getName())) { added.add(field.getName()); @@ -158,7 +146,7 @@ private void addFieldsFromType(JSTypeInfo type, List items, Se // Add from parent type with incremented depth if (type.getResolvedParent() != null) { - addFieldsFromType(type.getResolvedParent(), items, added, depth + 1, visited); + addFieldsFromType(type.getResolvedParent(), items, added, depth + 1); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index 416720f37..7efdd8481 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -89,25 +89,12 @@ public void setResolvedParent(JSTypeInfo parent) { * Get a method by name, including inherited methods. */ public JSMethodInfo getMethod(String name) { - return getMethod(name, new HashSet<>()); - } - - /** - * Internal method to get method with cycle detection. - */ - private JSMethodInfo getMethod(String name, Set visited) { - // Prevent infinite recursion from circular inheritance - if (visited.contains(this.fullName)) { - return null; - } - visited.add(this.fullName); - JSMethodInfo method = methods.get(name); if (method != null) return method; // Check parent if (resolvedParent != null) { - return resolvedParent.getMethod(name, visited); + return resolvedParent.getMethod(name); } return null; } @@ -116,21 +103,8 @@ private JSMethodInfo getMethod(String name, Set visited) { * Get all methods with a given name (for overloads). */ public List getMethodOverloads(String name) { - return getMethodOverloads(name, new HashSet<>()); - } - - /** - * Internal method to get method overloads with cycle detection. - */ - private List getMethodOverloads(String name, Set visited) { List overloads = new ArrayList<>(); - // Prevent infinite recursion from circular inheritance - if (visited.contains(this.fullName)) { - return overloads; - } - visited.add(this.fullName); - // Get from this type if (methods.containsKey(name)) { overloads.add(methods.get(name)); @@ -144,7 +118,7 @@ private List getMethodOverloads(String name, Set visited) // Get from parent if (resolvedParent != null) { - overloads.addAll(resolvedParent.getMethodOverloads(name, visited)); + overloads.addAll(resolvedParent.getMethodOverloads(name)); } return overloads; @@ -161,25 +135,12 @@ public boolean hasMethod(String name) { * Get a field by name, including inherited fields. */ public JSFieldInfo getField(String name) { - return getField(name, new HashSet<>()); - } - - /** - * Internal method to get field with cycle detection. - */ - private JSFieldInfo getField(String name, Set visited) { - // Prevent infinite recursion from circular inheritance - if (visited.contains(this.fullName)) { - return null; - } - visited.add(this.fullName); - JSFieldInfo field = fields.get(name); if (field != null) return field; // Check parent if (resolvedParent != null) { - return resolvedParent.getField(name, visited); + return resolvedParent.getField(name); } return null; } 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 dd497e465..8dfec22cf 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 @@ -368,16 +368,7 @@ public String getHookParameterType(String functionName) { public void resolveInheritance() { for (JSTypeInfo type : types.values()) { JSTypeInfo child = type; - Set visited = new HashSet<>(); while (child != null && child.getExtendsType() != null && child.getResolvedParent() == null) { - // Prevent circular inheritance - if (visited.contains(child.getFullName())) { - System.out.println("WARNING: Circular inheritance detected: " + child.getFullName() + - " extends " + child.getExtendsType()); - break; - } - visited.add(child.getFullName()); - JSTypeInfo parent = getType(child.getExtendsType()); if (parent != null) { child.setResolvedParent(parent); From b74a29dc29bdb63a139088d5251f3af8ea254240 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 22:47:13 +0200 Subject: [PATCH 237/337] Expanded namespace pattern --- .../js_parser/TypeScriptDefinitionParser.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 1d47e37fe..b3615cad8 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 @@ -24,8 +24,9 @@ public class TypeScriptDefinitionParser { private static final Pattern CLASS_PATTERN = Pattern.compile( "export\\s+class\\s+(\\w+)(?:<[^>]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + // Match both "export namespace" and plain "namespace" (for declare global blocks) private static final Pattern NAMESPACE_PATTERN = Pattern.compile( - "export\\s+namespace\\s+(\\w+)\\s*\\{"); + "(?:export\\s+)?namespace\\s+(\\w+)\\s*\\{"); // Make semicolon optional for type aliases (TypeScript doesn't require them) private static final Pattern TYPE_ALIAS_PATTERN = Pattern.compile( @@ -266,11 +267,13 @@ private void parseInterfaceFile(String content, String parentNamespace) { if (bodyEnd > bodyStart) { String body = content.substring(bodyStart, bodyEnd); - // Parse inner types (both exported and nested) with namespace prefix + // Parse inner types with namespace prefix + // Namespaces contain exported types, so use parseInterfaceFile String fullNamespace = parentNamespace != null ? parentNamespace + "." + namespaceName : namespaceName; parseInterfaceFile(body, fullNamespace); - parseNestedTypes(body, fullNamespace); + // Don't call parseNestedTypes here - namespace members are all exported + // and will be caught by parseInterfaceFile's INTERFACE_PATTERN } } From 57d63844c823d350963bfc2f874cf54b622faf17 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 22:58:09 +0200 Subject: [PATCH 238/337] Removed redundant unused .d.ts files --- .../customnpcs/api/scripts/item_events.d.ts | 24 ----- .../customnpcs/api/scripts/npc_events.d.ts | 32 ------- .../customnpcs/api/scripts/player_events.d.ts | 92 ------------------- 3 files changed, 148 deletions(-) delete mode 100644 src/main/resources/assets/customnpcs/api/scripts/item_events.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts diff --git a/src/main/resources/assets/customnpcs/api/scripts/item_events.d.ts b/src/main/resources/assets/customnpcs/api/scripts/item_events.d.ts deleted file mode 100644 index a02e87375..000000000 --- a/src/main/resources/assets/customnpcs/api/scripts/item_events.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/// -/// -/** - * Ambient declarations for `scripts_examples/item_events.js`. - * These map the function names used by CustomNPC+ item scripts to the - * precise event interface so `event.` autocompletes correctly inside - * each handler. This file is intended to be referenced from the script - * with a triple-slash reference comment. - */ - -declare function init(IItemEvent: IItemEvent.InitEvent): void; -declare function tick(IItemEvent: IItemEvent.UpdateEvent): void; -declare function tossed(IItemEvent: IItemEvent.TossedEvent): void; -declare function pickedUp(IItemEvent: IItemEvent.PickedUpEvent): void; -declare function spawn(IItemEvent: IItemEvent.SpawnEvent): void; -declare function interact(IItemEvent: IItemEvent.InteractEvent): void; -declare function rightClick(IItemEvent: IItemEvent.RightClickEvent): void; -declare function attack(IItemEvent: IItemEvent.AttackEvent): void; -declare function startItem(IItemEvent: IItemEvent.StartUsingItem): void; -declare function usingItem(IItemEvent: IItemEvent.UsingItem): void; -declare function stopItem(IItemEvent: IItemEvent.StopUsingItem): void; -declare function finishItem(IItemEvent: IItemEvent.FinishUsingItem): void; - -export {}; diff --git a/src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts b/src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts deleted file mode 100644 index f8e21f6f0..000000000 --- a/src/main/resources/assets/customnpcs/api/scripts/npc_events.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/// -/// -/** - * Ambient declarations for `scripts_examples/npc_events.js`. - * These map the function names used by CustomNPC+ NPC scripts to the - * precise event interface so `event.` autocompletes correctly inside - * each handler. Parameter names determine type inference: - * - Use `INpcEvent` for NPC-specific events - * - Use `IProjectileEvent` for projectile events - */ - -declare function init(INpcEvent: INpcEvent.InitEvent): void; -declare function tick(INpcEvent: INpcEvent.UpdateEvent): void; -declare function interact(INpcEvent: INpcEvent.InteractEvent): void; -declare function dialog(INpcEvent: INpcEvent.DialogEvent): void; -declare function damaged(INpcEvent: INpcEvent.DamagedEvent): void; -declare function killed(INpcEvent: INpcEvent.DiedEvent): void; -declare function meleeAttack(INpcEvent: INpcEvent.MeleeAttackEvent): void; -declare function meleeSwing(INpcEvent: INpcEvent.SwingEvent): void; -declare function rangedLaunched(INpcEvent: INpcEvent.RangedLaunchedEvent): void; -declare function target(INpcEvent: INpcEvent.TargetEvent): void; -declare function collide(INpcEvent: INpcEvent.CollideEvent): void; -declare function kills(INpcEvent: INpcEvent.KilledEntityEvent): void; -declare function dialogClose(INpcEvent: INpcEvent.DialogClosedEvent): void; -declare function timer(INpcEvent: INpcEvent.TimerEvent): void; -declare function targetLost(INpcEvent: INpcEvent.TargetLostEvent): void; - -// Projectile-specific -declare function projectileTick(IProjectileEvent: IProjectileEvent.UpdateEvent): void; -declare function projectileImpact(IProjectileEvent: IProjectileEvent.ImpactEvent): void; - -export {}; diff --git a/src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts b/src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts deleted file mode 100644 index e8e7669b2..000000000 --- a/src/main/resources/assets/customnpcs/api/scripts/player_events.d.ts +++ /dev/null @@ -1,92 +0,0 @@ -/// -/// -/** - * Ambient declarations for `scripts_examples/player_events.js`. - * These map the function names used by CustomNPC+ player scripts to the - * precise event interface so `event.` autocompletes correctly inside - * each handler. Parameter names determine type inference: - * - Use `IPlayerEvent` for player-specific events - * - Use `IQuestEvent`, `IFactionEvent`, `IDialogEvent`, etc. for specialized event types - */ - -declare function init(IPlayerEvent: IPlayerEvent.InitEvent): void; -declare function tick(IPlayerEvent: IPlayerEvent.UpdateEvent): void; -declare function interact(IPlayerEvent: IPlayerEvent.InteractEvent): void; -declare function rightClick(IPlayerEvent: IPlayerEvent.RightClickEvent): void; -declare function attack(IPlayerEvent: IPlayerEvent.AttackEvent): void; -declare function attacked(IPlayerEvent: IPlayerEvent.AttackedEvent): void; -declare function damagedEntity(IPlayerEvent: IPlayerEvent.DamagedEntityEvent): void; -declare function damaged(IPlayerEvent: IPlayerEvent.DamagedEvent): void; -declare function kills(IPlayerEvent: IPlayerEvent.KilledEntityEvent): void; -declare function killed(IPlayerEvent: IPlayerEvent.DiedEvent): void; -declare function drop(IPlayerEvent: IPlayerEvent.DropEvent): void; -declare function respawn(IPlayerEvent: IPlayerEvent.RespawnEvent): void; -declare function breakBlock(IPlayerEvent: IPlayerEvent.BreakEvent): void; -declare function chat(IPlayerEvent: IPlayerEvent.ChatEvent): void; -declare function login(IPlayerEvent: IPlayerEvent.LoginEvent): void; -declare function logout(IPlayerEvent: IPlayerEvent.LogoutEvent): void; -declare function keyPressed(IPlayerEvent: IPlayerEvent.KeyPressedEvent): void; -declare function mouseClicked(IPlayerEvent: IPlayerEvent.MouseClickedEvent): void; -declare function toss(IPlayerEvent: IPlayerEvent.TossEvent): void; -declare function pickUp(IPlayerEvent: IPlayerEvent.PickUpEvent): void; -declare function pickupXP(IPlayerEvent: IPlayerEvent.PickupXPEvent): void; -declare function rangedCharge(IPlayerEvent: IPlayerEvent.RangedChargeEvent): void; -declare function rangedLaunched(IPlayerEvent: IPlayerEvent.RangedLaunchedEvent): void; -declare function timer(IPlayerEvent: IPlayerEvent.TimerEvent): void; -declare function startItem(IPlayerEvent: IPlayerEvent.StartUsingItem): void; -declare function usingItem(IPlayerEvent: IPlayerEvent.UsingItem): void; -declare function stopItem(IPlayerEvent: IPlayerEvent.StopUsingItem): void; -declare function finishItem(IPlayerEvent: IPlayerEvent.FinishUsingItem): void; -declare function containerOpen(IPlayerEvent: IPlayerEvent.ContainerOpen): void; -declare function useHoe(IPlayerEvent: IPlayerEvent.UseHoeEvent): void; -declare function bonemeal(IPlayerEvent: IPlayerEvent.BonemealEvent): void; -declare function fillBucket(IPlayerEvent: IPlayerEvent.FillBucketEvent): void; -declare function jump(IPlayerEvent: IPlayerEvent.JumpEvent): void; -declare function fall(IPlayerEvent: IPlayerEvent.FallEvent): void; -declare function wakeUp(IPlayerEvent: IPlayerEvent.WakeUpEvent): void; -declare function sleep(IPlayerEvent: IPlayerEvent.SleepEvent): void; -declare function playSound(IPlayerEvent: IPlayerEvent.SoundEvent): void; -declare function lightning(IPlayerEvent: IPlayerEvent.LightningEvent): void; -declare function changedDim(IPlayerEvent: IPlayerEvent.ChangedDimension): void; - -// Quest / faction / dialog / custom events -declare function questStart(IQuestEvent: IQuestEvent.QuestStartEvent): void; -declare function questCompleted(IQuestEvent: IQuestEvent.QuestCompletedEvent): void; -declare function questTurnIn(IQuestEvent: IQuestEvent.QuestTurnedInEvent): void; -declare function factionPoints(IFactionEvent: IFactionEvent.FactionPoints): void; - -declare function dialogOpen(IDialogEvent: IDialogEvent.DialogClosed): void; -declare function dialogOption(IDialogEvent: IDialogEvent.DialogOption): void; -declare function dialogClose(IDialogEvent: IDialogEvent.DialogClosed): void; - -declare function scriptCommand(ICustomNPCsEvent: ICustomNPCsEvent.ScriptedCommandEvent): void; -declare function customGuiClosed(ICustomGuiEvent: ICustomGuiEvent.CloseEvent): void; -declare function customGuiButton(ICustomGuiEvent: ICustomGuiEvent.ButtonEvent): void; -declare function customGuiSlot(ICustomGuiEvent: ICustomGuiEvent.SlotEvent): void; -declare function customGuiSlotClicked(ICustomGuiEvent: ICustomGuiEvent.SlotClickEvent): void; -declare function customGuiScroll(ICustomGuiEvent: ICustomGuiEvent.ScrollEvent): void; - -// Party events -declare function partyQuestCompleted(IPartyEvent: IPartyEvent.PartyQuestCompletedEvent): void; -declare function partyQuestSet(IPartyEvent: IPartyEvent.PartyQuestSetEvent): void; -declare function partyQuestTurnedIn(IPartyEvent: IPartyEvent.PartyQuestTurnedInEvent): void; -declare function partyInvite(IPartyEvent: IPartyEvent.PartyInviteEvent): void; -declare function partyKick(IPartyEvent: IPartyEvent.PartyKickEvent): void; -declare function partyLeave(IPartyEvent: IPartyEvent.PartyLeaveEvent): void; -declare function partyDisband(IPartyEvent: IPartyEvent.PartyDisbandEvent): void; - -// Animation events -declare function animationStart(IAnimationEvent: IAnimationEvent.Started): void; -declare function animationEnd(IAnimationEvent: IAnimationEvent.Ended): void; -declare function frameEnter(IAnimationEvent: IAnimationEvent.IFrameEvent): void; -declare function frameExit(IAnimationEvent: IAnimationEvent.IFrameEvent): void; - -// Profile / effect events -declare function profileChange(IPlayerEvent: IPlayerEvent.ProfileEvent): void; -declare function profileRemove(IPlayerEvent: IPlayerEvent.ProfileEvent): void; -declare function profileCreate(IPlayerEvent: IPlayerEvent.ProfileEvent): void; -declare function onEffectAdd(IPlayerEvent: IPlayerEvent.EffectEvent): void; -declare function onEffectTick(IPlayerEvent: IPlayerEvent.EffectEvent): void; -declare function onEffectRemove(IPlayerEvent: IPlayerEvent.EffectEvent): void; - -export {}; From 8fb683ff2cc744d9eaa6ea2156fddff2a994e72f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 7 Jan 2026 23:30:15 +0200 Subject: [PATCH 239/337] Dynamic AutocompleteMenu dimension adjustment depending on available game window space above/below current line --- .../script/autocomplete/AutocompleteMenu.java | 72 +++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index 1916f4496..e8dea011d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -145,23 +145,71 @@ private void calculateDimensions(int cursorX, int cursorY, int viewportWidth, in } menuWidth = Math.min(maxItemWidth + 20, MAX_WIDTH); // +20 for scrollbar - // Calculate height + // Calculate initial height int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); int menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2; - // Position below cursor, or above if not enough space - this.x = cursorX; - this.y = cursorY; - - // Check if menu would go off-screen to the right - if (x + menuWidth > viewportWidth - 10) { - x = viewportWidth - menuWidth - 10; + // Line height estimation (cursor position is typically at baseline) + int lineHeight = 20; + + // Calculate available space + int spaceBelow = viewportHeight - cursorY - 10; // Space below cursor + int spaceAbove = cursorY - lineHeight - 10; // Space above current line + + // Determine if we should use horizontal positioning + // Use horizontal if both vertical positions would be cramped or block the cursor line + boolean useHorizontalPosition = false; + if (spaceBelow < menuHeight && spaceAbove < menuHeight) { + // Check if horizontal positioning would work better + int spaceRight = viewportWidth - (cursorX + 50) - 10; // 50 = offset from cursor + if (spaceRight >= menuWidth && viewportHeight > menuHeight + 20) { + useHorizontalPosition = true; + } else { + // Reduce height to fit in the best available vertical space + int availableVerticalSpace = Math.max(spaceBelow, spaceAbove); + if (availableVerticalSpace < menuHeight) { + // Calculate how many items we can show in available space + int maxItemsForSpace = (availableVerticalSpace - HINT_HEIGHT - PADDING * 2) / ITEM_HEIGHT; + maxItemsForSpace = Math.max(3, Math.min(maxItemsForSpace, items.size())); // At least 3 items + visibleItems = maxItemsForSpace; + menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2; + } + } } - // Check if menu would go off-screen to the bottom - if (y + menuHeight > viewportHeight - 10) { - // Show above cursor instead - y = cursorY - menuHeight - 20; // 20 for line height + // Position the menu + if (useHorizontalPosition) { + // Position to the right of the cursor + this.x = cursorX + 50; + this.y = cursorY - lineHeight; // Align with current line + + // Ensure doesn't go off bottom + if (y + menuHeight > viewportHeight - 10) { + y = viewportHeight - menuHeight - 10; + } + } else { + // Standard vertical positioning + this.x = cursorX; + + // Try below first + if (spaceBelow >= menuHeight) { + this.y = cursorY ; + } else if (spaceAbove >= menuHeight) { + // Show above cursor line + this.y = cursorY - menuHeight - lineHeight; + } else { + // Use the larger space + if (spaceBelow >= spaceAbove) { + this.y = cursorY; + } else { + this.y = cursorY - menuHeight - lineHeight; + } + } + + // Check if menu would go off-screen to the right + if (x + menuWidth > viewportWidth - 10) { + x = viewportWidth - menuWidth - 10; + } } // Ensure not off-screen to the left or top From 798e238804137576cb153c219528a855ba620edd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 01:26:01 +0200 Subject: [PATCH 240/337] Parsed generic type parameters in .d.ts files. i.e. IPlayer --- .../interpreter/js_parser/JSTypeInfo.java | 62 ++++++++++++++++ .../js_parser/TypeScriptDefinitionParser.java | 70 ++++++++++++++++--- 2 files changed, 124 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index 7efdd8481..5863d0412 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -8,10 +8,41 @@ */ public class JSTypeInfo { + /** + * Represents a generic type parameter like "T extends Entity". + * Stores the parameter name, the bound type, and optionally the full Java class name. + */ + public static class TypeParamInfo { + private final String name; // e.g., "T" + private final String boundType; // e.g., "EntityPlayerMP" + private final String fullBoundType; // e.g., "net.minecraft.entity.player.EntityPlayerMP" + + public TypeParamInfo(String name, String boundType, String fullBoundType) { + this.name = name; + this.boundType = boundType; + this.fullBoundType = fullBoundType; + } + + public String getName() { return name; } + public String getBoundType() { return boundType; } + public String getFullBoundType() { return fullBoundType; } + + @Override + public String toString() { + if (boundType != null) { + return name + " extends " + boundType + (fullBoundType != null ? " (" + fullBoundType + ")" : ""); + } + return name; + } + } + private final String simpleName; // e.g., "InteractEvent" private final String fullName; // e.g., "IPlayerEvent.InteractEvent" private final String namespace; // e.g., "IPlayerEvent" (parent namespace, null for top-level) + // Type parameters (generics) + private final List typeParams = new ArrayList<>(); + // Members private final Map methods = new LinkedHashMap<>(); private final Map fields = new LinkedHashMap<>(); @@ -72,6 +103,10 @@ public void setResolvedParent(JSTypeInfo parent) { this.resolvedParent = parent; } + public void addTypeParam(TypeParamInfo param) { + typeParams.add(param); + } + // Getters public String getSimpleName() { return simpleName; } public String getFullName() { return fullName; } @@ -80,11 +115,38 @@ public void setResolvedParent(JSTypeInfo parent) { public JSTypeInfo getResolvedParent() { return resolvedParent; } public String getDocumentation() { return documentation; } public JSTypeInfo getParentType() { return parentType; } + public List getTypeParams() { return typeParams; } public Map getMethods() { return methods; } public Map getFields() { return fields; } public Map getInnerTypes() { return innerTypes; } + /** + * Get the type parameter info for a given parameter name (e.g., "T"). + * @return TypeParamInfo or null if not found + */ + public TypeParamInfo getTypeParam(String name) { + for (TypeParamInfo param : typeParams) { + if (param.getName().equals(name)) { + return param; + } + } + return null; + } + + /** + * Resolves a type parameter to its bound type. + * For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("T") returns "EntityPlayerMP". + * If no type parameter is found with that name, returns the input. + */ + public String resolveTypeParam(String typeName) { + TypeParamInfo param = getTypeParam(typeName); + if (param != null && param.getBoundType() != null) { + return param.getBoundType(); + } + return typeName; + } + /** * Get a method by name, including inherited methods. */ 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 b3615cad8..5403f2ce8 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 @@ -12,17 +12,21 @@ public class TypeScriptDefinitionParser { // Patterns for parsing .d.ts content - // Updated to handle generic type parameters like: export interface IEntityLivingBase extends IEntity { + // Updated to capture generic type parameters like: export interface IEntity private static final Pattern INTERFACE_PATTERN = Pattern.compile( - "export\\s+interface\\s+(\\w+)(?:<[^>]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + "export\\s+interface\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); // Pattern for nested interfaces without export keyword private static final Pattern NESTED_INTERFACE_PATTERN = Pattern.compile( - "(?]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + "(?]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); // Similar pattern for classes private static final Pattern CLASS_PATTERN = Pattern.compile( - "export\\s+class\\s+(\\w+)(?:<[^>]*>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + "export\\s+class\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{"); + + // Pattern to parse individual type parameters like: T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP */ + private static final Pattern TYPE_PARAM_PATTERN = Pattern.compile( + "(\\w+)(?:\\s+extends\\s+(\\w+)(?:\\s*/\\*\\s*([\\w.]+)\\s*\\*/)?)?"); // Match both "export namespace" and plain "namespace" (for declare global blocks) private static final Pattern NAMESPACE_PATTERN = Pattern.compile( @@ -166,9 +170,16 @@ private void parseInterfaceFile(String content, String parentNamespace) { Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(content); while (interfaceMatcher.find()) { String interfaceName = interfaceMatcher.group(1); - String extendsClause = interfaceMatcher.group(2); + String typeParamsStr = interfaceMatcher.group(2); // e.g., "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP */" + String extendsClause = interfaceMatcher.group(3); JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace); + + // Parse type parameters + if (typeParamsStr != null && !typeParamsStr.isEmpty()) { + parseTypeParameters(typeParamsStr, typeInfo); + } + if (extendsClause != null) { // Handle multiple extends (e.g., "IEntityLivingBase, IAnimatable") // Take the first one, stripping generics @@ -204,9 +215,16 @@ private void parseInterfaceFile(String content, String parentNamespace) { Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content); while (nestedInterfaceMatcher.find()) { String interfaceName = nestedInterfaceMatcher.group(1); - String extendsClause = nestedInterfaceMatcher.group(2); + String typeParamsStr = nestedInterfaceMatcher.group(2); + String extendsClause = nestedInterfaceMatcher.group(3); JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace); + + // Parse type parameters + if (typeParamsStr != null && !typeParamsStr.isEmpty()) { + parseTypeParameters(typeParamsStr, typeInfo); + } + if (extendsClause != null) { String extendsType = extendsClause.trim(); extendsType = extendsType.replaceAll("<[^>]*>", ""); @@ -232,9 +250,16 @@ private void parseInterfaceFile(String content, String parentNamespace) { Matcher classMatcher = CLASS_PATTERN.matcher(content); while (classMatcher.find()) { String className = classMatcher.group(1); - String extendsClause = classMatcher.group(2); + String typeParamsStr = classMatcher.group(2); + String extendsClause = classMatcher.group(3); JSTypeInfo typeInfo = new JSTypeInfo(className, parentNamespace); + + // Parse type parameters + if (typeParamsStr != null && !typeParamsStr.isEmpty()) { + parseTypeParameters(typeParamsStr, typeInfo); + } + if (extendsClause != null) { String extendsType = extendsClause.trim(); extendsType = extendsType.replaceAll("<[^>]*>", ""); @@ -283,6 +308,28 @@ private void parseInterfaceFile(String content, String parentNamespace) { } } + /** + * Parse type parameters from a string like "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP *`/". + * Handles multiple parameters separated by commas. + */ + private void parseTypeParameters(String typeParamsStr, JSTypeInfo typeInfo) { + // Split by comma, but be careful with nested generics (shouldn't happen at this level, but be safe) + String[] params = typeParamsStr.split(","); + for (String param : params) { + param = param.trim(); + if (param.isEmpty()) continue; + + Matcher m = TYPE_PARAM_PATTERN.matcher(param); + if (m.find()) { + String name = m.group(1); + String boundType = m.group(2); + String fullBoundType = m.group(3); + + typeInfo.addTypeParam(new JSTypeInfo.TypeParamInfo(name, boundType, fullBoundType)); + } + } + } + /** * Parse nested types (interfaces and type aliases) within a parent type or namespace. */ @@ -291,9 +338,16 @@ private void parseNestedTypes(String content, String namespace) { Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content); while (nestedInterfaceMatcher.find()) { String interfaceName = nestedInterfaceMatcher.group(1); - String extendsClause = nestedInterfaceMatcher.group(2); + String typeParamsStr = nestedInterfaceMatcher.group(2); + String extendsClause = nestedInterfaceMatcher.group(3); JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, namespace); + + // Parse type parameters + if (typeParamsStr != null && !typeParamsStr.isEmpty()) { + parseTypeParameters(typeParamsStr, typeInfo); + } + if (extendsClause != null) { String extendsType = extendsClause.trim(); extendsType = extendsType.replaceAll("<[^>]*>", ""); From 7725d16e8223635d5f20ffd2bacd63c99c93d659 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 02:23:36 +0200 Subject: [PATCH 241/337] Implemented TypeParamInfo to TypeInfo and JSTypeInfo, and resolved all type params of JS API types after all are loaded --- .../interpreter/js_parser/JSTypeInfo.java | 73 ++++++++--------- .../interpreter/js_parser/JSTypeRegistry.java | 16 ++++ .../interpreter/js_parser/TypeParamInfo.java | 68 ++++++++++++++++ .../js_parser/TypeScriptDefinitionParser.java | 8 +- .../script/interpreter/type/TypeInfo.java | 80 ++++++++++++++++++- 5 files changed, 203 insertions(+), 42 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index 5863d0412..c7c06e715 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + import java.util.*; /** @@ -8,34 +10,6 @@ */ public class JSTypeInfo { - /** - * Represents a generic type parameter like "T extends Entity". - * Stores the parameter name, the bound type, and optionally the full Java class name. - */ - public static class TypeParamInfo { - private final String name; // e.g., "T" - private final String boundType; // e.g., "EntityPlayerMP" - private final String fullBoundType; // e.g., "net.minecraft.entity.player.EntityPlayerMP" - - public TypeParamInfo(String name, String boundType, String fullBoundType) { - this.name = name; - this.boundType = boundType; - this.fullBoundType = fullBoundType; - } - - public String getName() { return name; } - public String getBoundType() { return boundType; } - public String getFullBoundType() { return fullBoundType; } - - @Override - public String toString() { - if (boundType != null) { - return name + " extends " + boundType + (fullBoundType != null ? " (" + fullBoundType + ")" : ""); - } - return name; - } - } - private final String simpleName; // e.g., "InteractEvent" private final String fullName; // e.g., "IPlayerEvent.InteractEvent" private final String namespace; // e.g., "IPlayerEvent" (parent namespace, null for top-level) @@ -102,11 +76,7 @@ public void addInnerType(JSTypeInfo inner) { public void setResolvedParent(JSTypeInfo parent) { this.resolvedParent = parent; } - - public void addTypeParam(TypeParamInfo param) { - typeParams.add(param); - } - + // Getters public String getSimpleName() { return simpleName; } public String getFullName() { return fullName; } @@ -133,16 +103,43 @@ public TypeParamInfo getTypeParam(String name) { } return null; } + + public void addTypeParam(TypeParamInfo param) { + typeParams.add(param); + } + + /** + * Resolve all type parameters for this type. + * Called during Phase 2 after all types are loaded into the registry. + */ + public void resolveTypeParameters() { + for (TypeParamInfo param : typeParams) { + param.resolveBoundType(); + } + } /** - * Resolves a type parameter to its bound type. - * For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("T") returns "EntityPlayerMP". - * If no type parameter is found with that name, returns the input. + * Resolves a type parameter to its bound TypeInfo. + * For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("T") returns the TypeInfo for EntityPlayerMP. + * If no type parameter is found with that name, returns null. + */ + public TypeInfo resolveTypeParamToTypeInfo(String typeName) { + TypeParamInfo param = getTypeParam(typeName); + if (param != null) { + return param.getBoundTypeInfo(); + } + return null; + } + + /** + * Resolves a type parameter to its bound type name (for backward compatibility). + * @deprecated Use resolveTypeParamToTypeInfo instead */ + @Deprecated public String resolveTypeParam(String typeName) { TypeParamInfo param = getTypeParam(typeName); - if (param != null && param.getBoundType() != null) { - return param.getBoundType(); + if (param != null && param.getBoundTypeInfo() != null) { + return param.getBoundTypeInfo().getSimpleName(); } return typeName; } 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 8dfec22cf..a4565b5eb 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 @@ -83,6 +83,9 @@ public void initializeFromResources() { } } + // Phase 2: Resolve all type parameters now that all types are loaded + resolveAllTypeParameters(); + resolveInheritance(); registerEngineGlobalObjects(); @@ -223,6 +226,7 @@ public void initializeFromDirectory(File directory) { try { TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); parser.parseDirectory(directory); + resolveAllTypeParameters(); resolveInheritance(); initialized = true; System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks"); @@ -241,6 +245,7 @@ public void initializeFromVsix(File vsixFile) { try { TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); parser.parseVsixArchive(vsixFile); + resolveAllTypeParameters(); resolveInheritance(); initialized = true; System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from VSIX"); @@ -360,6 +365,17 @@ public String getHookParameterType(String functionName) { return null; } + /** + * Resolve all type parameters for all types. + * Called after all .d.ts files are loaded (Phase 2). + * This ensures that type parameters can reference any type in the registry. + */ + public void resolveAllTypeParameters() { + for (JSTypeInfo type : types.values()) { + type.resolveTypeParameters(); + } + } + /** * Resolve inheritance relationships between types. * For each type, walks up the parent chain and resolves all ancestors. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java new file mode 100644 index 000000000..d41005a6e --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java @@ -0,0 +1,68 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +/** + * Represents a generic type parameter like "T extends Entity". + * Stores the parameter name and both the string representation (for deferred resolution) + * and the resolved TypeInfo for the bound. + */ +public class TypeParamInfo { + private final String name; // e.g., "T" + private final String boundType; // Simple name like "EntityPlayerMP" (may be null) + private final String fullBoundType; // Full name like "net.minecraft.entity.player.EntityPlayerMP" (may be null) + private TypeInfo boundTypeInfo; // Resolved TypeInfo for the bound (resolved in Phase 2) + + /** + * Constructor for parse-time (Phase 1) - stores string names only. + */ + public TypeParamInfo(String name, String boundType, String fullBoundType) { + this.name = name; + this.boundType = boundType; + this.fullBoundType = fullBoundType; + this.boundTypeInfo = null; // Will be resolved later + } + + public String getName() { return name; } + + public TypeInfo getBoundTypeInfo() { + return boundTypeInfo; + } + + /** + * Resolve the bound type using TypeResolver. + * Called during Phase 2 after all types are loaded. + */ + public void resolveBoundType() { + if (boundTypeInfo != null) + return; // Already resolved + + if (fullBoundType != null && !fullBoundType.isEmpty()) { + // Try to load the Java class using the full name + boundTypeInfo = TypeResolver.getInstance().resolveFullName(fullBoundType); + } else if (boundType != null && !boundType.isEmpty()) { + // Try to resolve using the simple name + boundTypeInfo = TypeResolver.getInstance().resolveJSType(boundType); + } + } + + /** + * Get simple display name for the bound type. + */ + public String getBoundTypeName() { + if (boundTypeInfo == null) + return null; + return boundTypeInfo.getSimpleName(); + } + + @Override + public String toString() { + if (boundTypeInfo != null) { + return name + " extends " + boundTypeInfo.getSimpleName(); + } else if (boundType != null || fullBoundType != null) { + return name + " extends " + (boundType != null ? boundType : fullBoundType); + } + return name; + } +} 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 5403f2ce8..c507cf8f2 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 @@ -311,6 +311,7 @@ private void parseInterfaceFile(String content, String parentNamespace) { /** * Parse type parameters from a string like "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP *`/". * Handles multiple parameters separated by commas. + * Stores strings only - resolution happens in Phase 2 after all types are loaded. */ private void parseTypeParameters(String typeParamsStr, JSTypeInfo typeInfo) { // Split by comma, but be careful with nested generics (shouldn't happen at this level, but be safe) @@ -322,10 +323,11 @@ private void parseTypeParameters(String typeParamsStr, JSTypeInfo typeInfo) { Matcher m = TYPE_PARAM_PATTERN.matcher(param); if (m.find()) { String name = m.group(1); - String boundType = m.group(2); - String fullBoundType = m.group(3); + String boundType = m.group(2); // Simple name like "EntityPlayerMP" + String fullBoundType = m.group(3); // Full name like "net.minecraft.entity.player.EntityPlayerMP" - typeInfo.addTypeParam(new JSTypeInfo.TypeParamInfo(name, boundType, fullBoundType)); + // Store strings only - resolution happens in Phase 2 + typeInfo.addTypeParam(new TypeParamInfo(name, boundType, fullBoundType)); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index cc192e38c..d00a84c32 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -5,6 +5,7 @@ import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; @@ -60,6 +61,9 @@ public enum Kind { // JavaScript/TypeScript type info (for types from .d.ts files) private final JSTypeInfo jsTypeInfo; // The JS type info (null if Java type) + + // Type parameters (generics) + private final List typeParams = new ArrayList<>(); private TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { @@ -477,7 +481,8 @@ public java.util.List getAllMethodOverloads(String methodName) { // Check JS type first if (jsTypeInfo != null) { for (JSMethodInfo jsMethod : jsTypeInfo.getMethodOverloads(methodName)) { - overloads.add(MethodInfo.fromJSMethod(jsMethod, this)); + // Pass jsTypeInfo as context for type parameter resolution + overloads.add(MethodInfo.fromJSMethod(jsMethod, this, jsTypeInfo)); } return overloads; } @@ -679,6 +684,79 @@ public FieldInfo getFieldInfo(String fieldName) { public void validate() { // Default: no validation for Java types } + + // ==================== Type Parameter Methods ==================== + + /** + * Add a type parameter to this type. + * Used during parsing/construction of types with generics. + */ + public void addTypeParam(TypeParamInfo param) { + // If this is a JS type, delegate to JSTypeInfo + if (jsTypeInfo != null) { + jsTypeInfo.addTypeParam(param); + } else { + typeParams.add(param); + } + } + + /** + * Get all type parameters for this type. + * @return List of type parameters (empty if none) + */ + public List getTypeParams() { + // If this is a JS type, delegate to JSTypeInfo + if (jsTypeInfo != null) { + return jsTypeInfo.getTypeParams(); + } + return typeParams; + } + + /** + * Resolve all type parameters for this type. + * Called during Phase 2 after all types are loaded into the registry. + */ + public void resolveTypeParameters() { + // If this is a JS type, delegate to JSTypeInfo + if (jsTypeInfo != null) { + jsTypeInfo.resolveTypeParameters(); + } else { + for (TypeParamInfo param : typeParams) { + param.resolveBoundType(); + } + } + } + + /** + * Get the type parameter info for a given parameter name (e.g., "T"). + * @return TypeParamInfo or null if not found + */ + public TypeParamInfo getTypeParam(String name) { + // If this is a JS type, delegate to JSTypeInfo + if (jsTypeInfo != null) { + return jsTypeInfo.getTypeParam(name); + } + + for (TypeParamInfo param : typeParams) { + if (param.getName().equals(name)) { + return param; + } + } + return null; + } + + /** + * Resolves a type parameter to its bound TypeInfo. + * For example, if this type has "T extends EntityPlayerMP", resolveTypeParamToTypeInfo("T") returns the TypeInfo for EntityPlayerMP. + * If no type parameter is found with that name, returns null. + */ + public TypeInfo resolveTypeParamToTypeInfo(String typeName) { + TypeParamInfo param = getTypeParam(typeName); + if (param != null) { + return param.getBoundTypeInfo(); + } + return null; + } @Override public String toString() { From 2632ff4bc6336c5eab92b80c10c83a5bfd894fd6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 02:42:01 +0200 Subject: [PATCH 242/337] Added type parameter TypeInfo fetching to MethodInfo --- .../script/interpreter/method/MethodInfo.java | 71 +++++++------------ .../script/interpreter/type/TypeInfo.java | 4 +- 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index e266ac824..33c4b7ea5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -175,9 +175,19 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T */ public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containingType) { String name = jsMethod.getName(); + noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver resolver = noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver.getInstance(); - // Resolve the return type from the JS type registry - TypeInfo returnType = resolveJSType(jsMethod.getReturnType()); + // Resolve the return type - first from TypeResolver, then check for type parameters + String returnTypeName = jsMethod.getReturnType(); + TypeInfo returnType = resolver.resolveJSType(returnTypeName); + + // If not resolved and containingType has type parameters, try to resolve as type parameter + if (containingType != null && !returnType.isResolved()) { + TypeInfo paramResolution = containingType.resolveTypeParamToTypeInfo(returnTypeName); + if (paramResolution != null) { + returnType = paramResolution; + } + } // Convert JS parameters to FieldInfo List params = new ArrayList<>(); @@ -186,7 +196,18 @@ public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containing for (JSMethodInfo.JSParameterInfo param : jsParams) { String paramName = param.getName(); String paramTypeName = param.getType(); - TypeInfo paramType = resolveJSType(paramTypeName); + + // Resolve parameter type - first from TypeResolver, then check for type parameters + TypeInfo paramType = resolver.resolveJSType(paramTypeName); + + // If not resolved and containingType has type parameters, try to resolve as type parameter + if (containingType != null && !paramType.isResolved()) { + TypeInfo paramResolution = containingType.resolveTypeParamToTypeInfo(paramTypeName); + if (paramResolution != null) { + paramType = paramResolution; + } + } + params.add(FieldInfo.reflectionParam(paramName, paramType)); } @@ -199,49 +220,7 @@ public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containing return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, null); } - /** - * Resolves a JavaScript type name to a TypeInfo. - * Handles primitives, mapped types, and custom types from the registry. - */ - private static TypeInfo resolveJSType(String jsTypeName) { - if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) { - return TypeInfo.fromPrimitive("void"); - } - - // Handle JS primitives - switch (jsTypeName) { - case "string": - return TypeInfo.fromClass(String.class); - case "number": - return TypeInfo.fromClass(double.class); - case "boolean": - return TypeInfo.fromClass(boolean.class); - case "any": - return TypeInfo.fromClass(Object.class); - case "void": - return TypeInfo.fromPrimitive("void"); - } - - // Handle array types - if (jsTypeName.endsWith("[]")) { - String elementType = jsTypeName.substring(0, jsTypeName.length() - 2); - TypeInfo elementTypeInfo = resolveJSType(elementType); - // For arrays, we create an array type representation - return TypeInfo.arrayOf(elementTypeInfo); - } - - // Try to resolve from the JS type registry - JSTypeRegistry registry = JSTypeRegistry.getInstance(); - if (registry != null) { - JSTypeInfo jsTypeInfo = registry.getType(jsTypeName); - if (jsTypeInfo != null) { - return TypeInfo.fromJSTypeInfo(jsTypeInfo); - } - } - - // Fallback: unresolved type - return TypeInfo.unresolved(jsTypeName, jsTypeName); - } + // Getters public String getName() { return name; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index d00a84c32..5983012de 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -481,8 +481,8 @@ public java.util.List getAllMethodOverloads(String methodName) { // Check JS type first if (jsTypeInfo != null) { for (JSMethodInfo jsMethod : jsTypeInfo.getMethodOverloads(methodName)) { - // Pass jsTypeInfo as context for type parameter resolution - overloads.add(MethodInfo.fromJSMethod(jsMethod, this, jsTypeInfo)); + // Type parameter resolution now happens inside fromJSMethod using this TypeInfo + overloads.add(MethodInfo.fromJSMethod(jsMethod, this)); } return overloads; } From 99f7d0594801b966643c9338554b3dabcdae3662 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 03:14:44 +0200 Subject: [PATCH 243/337] =?UTF-8?q?Properly=20resolved=20type=20parameters?= =?UTF-8?q?=20for=20autocomplete=20display=20(e.g.,=20T=20=E2=86=92=20Enti?= =?UTF-8?q?tyPlayerMP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../script/autocomplete/AutocompleteItem.java | 47 +++++++++++++++++-- .../autocomplete/JSAutocompleteProvider.java | 41 +++++++++------- 2 files changed, 67 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index b845aae21..45c9ae31c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -200,7 +200,7 @@ public static AutocompleteItem fromType(TypeInfo type) { * Create from a JavaScript JSMethodInfo. */ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { - return fromJSMethod(method, 0); + return fromJSMethod(method, null, 0); } /** @@ -209,6 +209,16 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) */ public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritanceDepth) { + return fromJSMethod(method, null, inheritanceDepth); + } + + /** + * Create from a JavaScript JSMethodInfo with type parameter resolution for display. + * @param method The method info + * @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP) + * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) + */ + public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contextType, int inheritanceDepth) { String name = method.getName(); StringBuilder insertText = new StringBuilder(name); insertText.append("("); @@ -220,14 +230,22 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritance int returnIndex = sig.lastIndexOf(":"); if (returnIndex != -1) //remove ":ReturnType" displayName = sig.substring(0, returnIndex); - + + // Resolve type parameters for display (e.g., T → EntityPlayerMP) + String returnType = method.getReturnType(); + if (contextType != null) { + TypeInfo resolved = contextType.resolveTypeParamToTypeInfo(returnType); + if (resolved != null && resolved.isResolved()) + returnType = getName(resolved); + + } return new AutocompleteItem( displayName, name, // Search against just the method name insertText.toString(), Kind.METHOD, - method.getReturnType(), + returnType, method.getSignature(), method.getDocumentation(), method, @@ -242,7 +260,7 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritance * Create from a JavaScript JSFieldInfo. */ public static AutocompleteItem fromJSField(JSFieldInfo field) { - return fromJSField(field, 0); + return fromJSField(field, null, 0); } /** @@ -251,12 +269,31 @@ public static AutocompleteItem fromJSField(JSFieldInfo field) { * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) */ public static AutocompleteItem fromJSField(JSFieldInfo field, int inheritanceDepth) { + return fromJSField(field, null, inheritanceDepth); + } + + /** + * Create from a JavaScript JSFieldInfo with type parameter resolution for display. + * @param field The field info + * @param contextType The TypeInfo context for resolving type parameters + * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) + */ + public static AutocompleteItem fromJSField(JSFieldInfo field, TypeInfo contextType, int inheritanceDepth) { + // Resolve type parameters for display (e.g., T → EntityPlayerMP) + String fieldType = field.getType(); + if (contextType != null) { + TypeInfo resolved = contextType.resolveTypeParamToTypeInfo(fieldType); + if (resolved != null && resolved.isResolved()) + fieldType = getName(resolved); + + } + return new AutocompleteItem( field.getName(), field.getName(), // searchName same as display name field.getName(), Kind.FIELD, - field.getType(), + fieldType, field.toString(), field.getDocumentation(), field, diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index e9217a4d3..9aa839654 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -49,10 +49,9 @@ protected void addMemberSuggestions(Context context, List item // For JS types, check JSTypeRegistry JSTypeInfo jsTypeInfo = receiverType.getJSTypeInfo(); if (jsTypeInfo != null) { - // Add methods - addMethodsFromType(jsTypeInfo, items, new HashSet<>()); - // Add fields - addFieldsFromType(jsTypeInfo, items, new HashSet<>()); + // Pass both: jsTypeInfo (current type in hierarchy) and receiverType (context for type params) + addMethodsFromType(jsTypeInfo, receiverType, items, new HashSet<>()); + addFieldsFromType(jsTypeInfo, receiverType, items, new HashSet<>()); return; } @@ -99,15 +98,19 @@ protected void addMemberSuggestions(Context context, List item /** * Recursively add methods from a type and its parents. + * @param type The current JS type to get methods from (changes as we walk up inheritance) + * @param contextType The original TypeInfo context for resolving type parameters (stays constant) */ - protected void addMethodsFromType(JSTypeInfo type, List items, Set added) { - addMethodsFromType(type, items, added, 0); + protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added) { + addMethodsFromType(type, contextType, items, added, 0); } /** * Recursively add methods from a type and its parents with inheritance depth tracking. + * @param type The current JS type in inheritance chain + * @param contextType The original TypeInfo context for resolving type parameters (e.g., IPlayer with T → EntityPlayerMP) */ - private void addMethodsFromType(JSTypeInfo type, List items, Set added, int depth) { + private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added, int depth) { for (JSMethodInfo method : type.getMethods().values()) { String name = method.getName(); // Skip overload markers (name$1, name$2, etc.) @@ -116,37 +119,43 @@ private void addMethodsFromType(JSTypeInfo type, List items, S } if (!added.contains(name)) { added.add(name); - items.add(AutocompleteItem.fromJSMethod(method, depth)); + // Pass contextType (original receiver) for type parameter resolution, not current type + items.add(AutocompleteItem.fromJSMethod(method, contextType, depth)); } } - // Add from parent type with incremented depth + // Add from parent type with incremented depth - keep same contextType if (type.getResolvedParent() != null) { - addMethodsFromType(type.getResolvedParent(), items, added, depth + 1); + addMethodsFromType(type.getResolvedParent(), contextType, items, added, depth + 1); } } /** * Recursively add fields from a type and its parents. + * @param type The current JS type to get fields from (changes as we walk up inheritance) + * @param contextType The original TypeInfo context for resolving type parameters (stays constant) */ - protected void addFieldsFromType(JSTypeInfo type, List items, Set added) { - addFieldsFromType(type, items, added, 0); + protected void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added) { + addFieldsFromType(type, contextType, items, added, 0); } /** * Recursively add fields from a type and its parents with inheritance depth tracking. + * @param type The current JS type in inheritance chain + * @param contextType The original TypeInfo context for resolving type parameters */ - private void addFieldsFromType(JSTypeInfo type, List items, Set added, int depth) { + private void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added, int depth) { for (JSFieldInfo field : type.getFields().values()) { if (!added.contains(field.getName())) { added.add(field.getName()); - items.add(AutocompleteItem.fromJSField(field, depth)); + // Pass contextType (original receiver) for type parameter resolution, not current type + items.add(AutocompleteItem.fromJSField(field, contextType, depth)); } } - // Add from parent type with incremented depth + // Add from parent type with incremented depth - keep same contextType if (type.getResolvedParent() != null) { - addFieldsFromType(type.getResolvedParent(), items, added, depth + 1); + addFieldsFromType(type.getResolvedParent(), contextType, items, added, depth + 1); } } From 869dda34a5f7f55fbf7fc2c96995b9c1e8deb4cc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 04:09:18 +0200 Subject: [PATCH 244/337] -Java class types can are now functional in JS API. (Autocomplete and marking) -Revamped JSMethodInfo/ParamInfo/FieldInfo/TypeInfo to allow for the above change --- .../interpreter/js_parser/JSFieldInfo.java | 54 ++++++++++- .../interpreter/js_parser/JSMethodInfo.java | 96 ++++++++++++++++++- .../interpreter/js_parser/JSTypeInfo.java | 30 ++++++ .../interpreter/js_parser/JSTypeRegistry.java | 15 +++ .../script/interpreter/method/MethodInfo.java | 27 +----- .../script/interpreter/type/TypeResolver.java | 14 ++- 6 files changed, 210 insertions(+), 26 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java index 4ff653071..fc1ccd642 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java @@ -1,14 +1,18 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; /** * Represents a field/property in a TypeScript interface. */ public class JSFieldInfo { private final String name; - private final String type; // Raw type string + private final String type; // Raw type string for display + private TypeInfo typeInfo; // Resolved TypeInfo private final boolean readonly; private String documentation; + private JSTypeInfo containingType; // The type that contains this field public JSFieldInfo(String name, String type, boolean readonly) { this.name = name; @@ -21,11 +25,59 @@ public JSFieldInfo setDocumentation(String documentation) { return this; } + + /** + * Set the containing type (the JSTypeInfo that owns this field). + */ + public void setContainingType(JSTypeInfo containingType) { + this.containingType = containingType; + } + + public void setTypeInfo(TypeInfo typeInfo) { + this.typeInfo = typeInfo; + } + + /** + * Get the resolved type, with fallback resolution using type parameters. + * @param contextType The TypeInfo context for resolving type parameters + * @return The resolved TypeInfo for the field type + */ + public TypeInfo getResolvedType(TypeInfo contextType) { + // Use pre-resolved if available + if (typeInfo != null && typeInfo.isResolved()) + return typeInfo; + + // Fall back to resolving from string + TypeResolver resolver = TypeResolver.getInstance(); + TypeInfo resolved = typeInfo = resolver.resolveJSType(type); + + // If not resolved and contextType has type parameters, try to resolve as type parameter + if (contextType != null && !resolved.isResolved()) { + TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(type); + if (paramResolution != null) + resolved = typeInfo = paramResolution; // cache it to typeInfo + } + + return resolved; + } + // Getters public String getName() { return name; } public String getType() { return type; } + public TypeInfo getTypeInfo() { return typeInfo; } public boolean isReadonly() { return readonly; } public String getDocumentation() { return documentation; } + public JSTypeInfo getContainingType() { return containingType; } + + /** + * Get display name - uses resolved TypeInfo simple name if available. + */ + public String getDisplayType() { + if (typeInfo != null && typeInfo.isResolved()) { + return typeInfo.getSimpleName(); + } + return type; + } /** * Build hover info HTML for this field. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java index 2d5c03d1d..8999582de 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java @@ -1,5 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + import java.util.*; /** @@ -8,9 +11,11 @@ public class JSMethodInfo { private final String name; - private final String returnType; // Raw type string, e.g., "number", "IEntity", "void" + private final String returnType; // Raw type string for display, e.g., "number", "IEntity", "void" + private TypeInfo returnTypeInfo; // Resolved TypeInfo (set during Phase 2) private final List parameters; private String documentation; + private JSTypeInfo containingType; // The type that contains this method public JSMethodInfo(String name, String returnType, List parameters) { this.name = name; @@ -22,13 +27,56 @@ public JSMethodInfo setDocumentation(String documentation) { this.documentation = documentation; return this; } + + /** + * Set the resolved return type info (called during Phase 2 type resolution). + */ + public void setReturnTypeInfo(TypeInfo typeInfo) { + this.returnTypeInfo = typeInfo; + } + + /** + * Set the containing type (the JSTypeInfo that owns this method). + */ + public void setContainingType(JSTypeInfo containingType) { + this.containingType = containingType; + // Also set for all parameters + for (JSParameterInfo param : parameters) + param.setContainingMethod(this); + } + + /** + * Get the resolved return type, with fallback resolution using type parameters. + * @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP) + * @return The resolved TypeInfo for the return type + */ + public TypeInfo getResolvedReturnType(TypeInfo contextType) { + // Use pre-resolved if available + if (returnTypeInfo != null && returnTypeInfo.isResolved()) + return returnTypeInfo; + + // Fall back to resolving from string + TypeResolver resolver = TypeResolver.getInstance(); + TypeInfo resolved = returnTypeInfo = resolver.resolveJSType(returnType); + + // If not resolved and contextType has type parameters, try to resolve as type parameter + if (contextType != null && !resolved.isResolved()) { + TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(returnType); + if (paramResolution != null) + resolved = returnTypeInfo = paramResolution; //cache it to returnTypeInfo + } + + return resolved; + } // Getters public String getName() { return name; } public String getReturnType() { return returnType; } + public TypeInfo getReturnTypeInfo() { return returnTypeInfo; } public List getParameters() { return parameters; } public String getDocumentation() { return documentation; } public int getParameterCount() { return parameters.size(); } + public JSTypeInfo getContainingType() { return containingType; } /** * Get a formatted signature string for display. @@ -76,14 +124,60 @@ public String toString() { public static class JSParameterInfo { private final String name; private final String type; + private TypeInfo typeInfo; // Resolved TypeInfo (set during Phase 2) + private JSMethodInfo containingMethod; // The type that contains the method with this parameter public JSParameterInfo(String name, String type) { this.name = name; this.type = type; } + + public void setContainingMethod(JSMethodInfo containingMethod) { + this.containingMethod = containingMethod; + } + + public void setTypeInfo(TypeInfo typeInfo) { + this.typeInfo = typeInfo; + } + + /** + * Get the resolved type, with fallback resolution using type parameters. + * @param contextType The TypeInfo context for resolving type parameters + * @return The resolved TypeInfo for the parameter type + */ + public TypeInfo getResolvedType(TypeInfo contextType) { + // Use pre-resolved if available + if (typeInfo != null && typeInfo.isResolved()) + return typeInfo; + + // Fall back to resolving from string + TypeResolver resolver = TypeResolver.getInstance(); + TypeInfo resolved = typeInfo = resolver.resolveJSType(type); + + // If not resolved and contextType has type parameters, try to resolve as type parameter + if (contextType != null && !resolved.isResolved()) { + TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(type); + if (paramResolution != null) + resolved = typeInfo = paramResolution; //cache it to typeInfo + } + + return resolved; + } public String getName() { return name; } public String getType() { return type; } + public TypeInfo getTypeInfo() {return typeInfo;} + public JSMethodInfo getContainingMethod() {return containingMethod;} + + /** + * Get display name - uses resolved TypeInfo simple name if available. + */ + public String getDisplayType() { + if (typeInfo != null && typeInfo.isResolved()) { + return typeInfo.getSimpleName(); + } + return type; + } @Override public String toString() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index c7c06e715..f25e63a6b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; import java.util.*; @@ -50,6 +51,7 @@ public JSTypeInfo setDocumentation(String documentation) { } public void addMethod(JSMethodInfo method) { + method.setContainingType(this); // Handle overloads - store with index if name already exists String key = method.getName(); if (methods.containsKey(key)) { @@ -65,6 +67,7 @@ public void addMethod(JSMethodInfo method) { } public void addField(JSFieldInfo field) { + field.setContainingType(this); fields.put(field.getName(), field); } @@ -118,6 +121,33 @@ public void resolveTypeParameters() { } } + /** + * Resolve all member types (return types, field types, parameter types). + * Called during Phase 2 after all types are loaded into the registry. + * This resolves types like "Java.java.io.File" to proper TypeInfo objects. + */ + public void resolveMemberTypes() { + TypeResolver resolver = TypeResolver.getInstance(); + + // Resolve method return types and parameter types + for (JSMethodInfo method : methods.values()) { + TypeInfo returnTypeInfo = resolver.resolveJSType(method.getReturnType()); + method.setReturnTypeInfo(returnTypeInfo); + + // Resolve parameter types + for (JSMethodInfo.JSParameterInfo param : method.getParameters()) { + TypeInfo paramTypeInfo = resolver.resolveJSType(param.getType()); + param.setTypeInfo(paramTypeInfo); + } + } + + // Resolve field types + for (JSFieldInfo field : fields.values()) { + TypeInfo fieldTypeInfo = resolver.resolveJSType(field.getType()); + field.setTypeInfo(fieldTypeInfo); + } + } + /** * Resolves a type parameter to its bound TypeInfo. * For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("T") returns the TypeInfo for EntityPlayerMP. 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 a4565b5eb..5d4dfe15c 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 @@ -86,6 +86,9 @@ public void initializeFromResources() { // Phase 2: Resolve all type parameters now that all types are loaded resolveAllTypeParameters(); + // Phase 2b: Resolve member types (return types, field types, param types) + resolveAllMemberTypes(); + resolveInheritance(); registerEngineGlobalObjects(); @@ -227,6 +230,7 @@ public void initializeFromDirectory(File directory) { TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); parser.parseDirectory(directory); resolveAllTypeParameters(); + resolveAllMemberTypes(); resolveInheritance(); initialized = true; System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks"); @@ -246,6 +250,7 @@ public void initializeFromVsix(File vsixFile) { TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); parser.parseVsixArchive(vsixFile); resolveAllTypeParameters(); + resolveAllMemberTypes(); resolveInheritance(); initialized = true; System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from VSIX"); @@ -376,6 +381,16 @@ public void resolveAllTypeParameters() { } } + /** + * Resolve all member types (return types, field types, parameter types) for all types. + * Called after resolveAllTypeParameters (Phase 2b). + * This resolves types like "Java.java.io.File" to proper TypeInfo objects. + */ + public void resolveAllMemberTypes() { + for (JSTypeInfo type : types.values()) + type.resolveMemberTypes(); + } + /** * Resolve inheritance relationships between types. * For each type, walks up the parent chain and resolves all ancestors. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 33c4b7ea5..039127ea5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -175,19 +175,9 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T */ public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containingType) { String name = jsMethod.getName(); - noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver resolver = noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver.getInstance(); - // Resolve the return type - first from TypeResolver, then check for type parameters - String returnTypeName = jsMethod.getReturnType(); - TypeInfo returnType = resolver.resolveJSType(returnTypeName); - - // If not resolved and containingType has type parameters, try to resolve as type parameter - if (containingType != null && !returnType.isResolved()) { - TypeInfo paramResolution = containingType.resolveTypeParamToTypeInfo(returnTypeName); - if (paramResolution != null) { - returnType = paramResolution; - } - } + // Use the new getResolvedReturnType method + TypeInfo returnType = jsMethod.getResolvedReturnType(containingType); // Convert JS parameters to FieldInfo List params = new ArrayList<>(); @@ -195,18 +185,9 @@ public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containing for (JSMethodInfo.JSParameterInfo param : jsParams) { String paramName = param.getName(); - String paramTypeName = param.getType(); - // Resolve parameter type - first from TypeResolver, then check for type parameters - TypeInfo paramType = resolver.resolveJSType(paramTypeName); - - // If not resolved and containingType has type parameters, try to resolve as type parameter - if (containingType != null && !paramType.isResolved()) { - TypeInfo paramResolution = containingType.resolveTypeParamToTypeInfo(paramTypeName); - if (paramResolution != null) { - paramType = paramResolution; - } - } + // Use the new getResolvedType method + TypeInfo paramType = param.getResolvedType(containingType); params.add(FieldInfo.reflectionParam(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 228c26040..81349ef52 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 @@ -129,7 +129,7 @@ public void clearCache() { * Resolve a type name for JavaScript context. * Handles JS primitives, .d.ts types, and falls back to Java types. * - * @param jsTypeName The JS type name (e.g., "IPlayer", "string", "number") + * @param jsTypeName The JS type name (e.g., "IPlayer", "string", "number", "Java.java.io.File") * @return TypeInfo for the resolved type, or unresolved TypeInfo if not found */ public TypeInfo resolveJSType(String jsTypeName) { @@ -148,6 +148,18 @@ public TypeInfo resolveJSType(String jsTypeName) { // Strip array brackets for base type resolution boolean isArray = jsTypeName.endsWith("[]"); String baseName = isArray ? jsTypeName.substring(0, jsTypeName.length() - 2) : jsTypeName; + + // Handle "Java." prefix - convert to actual Java type + // e.g., "Java.java.io.File" -> "java.io.File" + if (baseName.startsWith("Java.")) { + String javaFullName = baseName.substring(5); // Remove "Java." + TypeInfo javaType = resolveFullName(javaFullName); + if (javaType != null && javaType.isResolved()) { + return isArray ? TypeInfo.arrayOf(javaType) : javaType; + } + // Still return unresolved with the cleaned-up name + return TypeInfo.unresolved(javaFullName.substring(javaFullName.lastIndexOf('.') + 1), javaFullName); + } switch (baseName.toLowerCase()) { case "string": From e45f6b7da5083286c7231eb808dfc796265a659b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 04:19:54 +0200 Subject: [PATCH 245/337] Forgot this from last commit --- .../script/autocomplete/AutocompleteItem.java | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 45c9ae31c..8db4ea073 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -224,24 +224,26 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contex insertText.append("("); insertText.append(")"); - // Extract display name from signature: remove return type prefix and keep only name(params) - String displayName = name; - String sig = method.getSignature(); - int returnIndex = sig.lastIndexOf(":"); - if (returnIndex != -1) //remove ":ReturnType" - displayName = sig.substring(0, returnIndex); - - // Resolve type parameters for display (e.g., T → EntityPlayerMP) - String returnType = method.getReturnType(); - if (contextType != null) { - TypeInfo resolved = contextType.resolveTypeParamToTypeInfo(returnType); - if (resolved != null && resolved.isResolved()) - returnType = getName(resolved); + // Build display name with resolved parameter types + StringBuilder displayName = new StringBuilder(name); + displayName.append("("); + for (int i = 0; i < method.getParameters().size(); i++) { + if (i > 0) displayName.append(", "); + JSMethodInfo.JSParameterInfo param = method.getParameters().get(i); + // Use resolved type + TypeInfo paramType = param.getResolvedType(contextType); + String paramTypeName = paramType.isResolved() ? paramType.getSimpleName() : param.getType(); + displayName.append(paramTypeName).append(" ").append(param.getName()); } + displayName.append(")"); + + // Get return type using the new method + TypeInfo returnTypeInfo = method.getResolvedReturnType(contextType); + String returnType = returnTypeInfo.isResolved() ? returnTypeInfo.getSimpleName() : method.getReturnType(); return new AutocompleteItem( - displayName, + displayName.toString(), name, // Search against just the method name insertText.toString(), Kind.METHOD, @@ -279,14 +281,9 @@ public static AutocompleteItem fromJSField(JSFieldInfo field, int inheritanceDep * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) */ public static AutocompleteItem fromJSField(JSFieldInfo field, TypeInfo contextType, int inheritanceDepth) { - // Resolve type parameters for display (e.g., T → EntityPlayerMP) - String fieldType = field.getType(); - if (contextType != null) { - TypeInfo resolved = contextType.resolveTypeParamToTypeInfo(fieldType); - if (resolved != null && resolved.isResolved()) - fieldType = getName(resolved); - - } + // Get field type using the new method + TypeInfo fieldTypeInfo = field.getResolvedType(contextType); + String fieldType = fieldTypeInfo.isResolved() ? fieldTypeInfo.getSimpleName() : field.getType(); return new AutocompleteItem( field.getName(), From 9eb30f862a8951bef0993b727c37c18700cecb8e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 04:20:39 +0200 Subject: [PATCH 246/337] Fixed AutocompleteManager breaking in findReceiverExpression if it contains quotes "" --- .../script/autocomplete/AutocompleteManager.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 4abfa173a..60f352743 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -663,6 +663,19 @@ private String findReceiverExpression(String text, int dotPos) { } else { break; } + } else if (c == '"' || c == '\'') { + // Skip over string literals + char quote = c; + start--; + while (start > 0) { + char strChar = text.charAt(start - 1); + if (strChar == quote && (start < 2 || text.charAt(start - 2) != '\\')) { + // Found unescaped closing quote + start--; + break; + } + start--; + } } else if (Character.isJavaIdentifierPart(c) || c == '.') { start--; } else if (Character.isWhitespace(c)) { From 57906790484881dbb071cdbae70467a920ee7fc3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 04:48:16 +0200 Subject: [PATCH 247/337] Properly setup autocompletion member suggestion for java classes in JS API --- .../autocomplete/JSAutocompleteProvider.java | 51 ++++--------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index 9aa839654..d0c85a3fe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -46,53 +46,20 @@ protected void addMemberSuggestions(Context context, List item return; } - // For JS types, check JSTypeRegistry + // If this is a Java type (has a Java class), delegate to parent's Java handling + // This preserves static/instance context, modifiers, and all Java-specific behavior + Class javaClass = receiverType.getJavaClass(); + if (javaClass != null) { + super.addMemberSuggestions(context, items); + return; + } + + // For pure JS types, check JSTypeRegistry JSTypeInfo jsTypeInfo = receiverType.getJSTypeInfo(); if (jsTypeInfo != null) { // Pass both: jsTypeInfo (current type in hierarchy) and receiverType (context for type params) addMethodsFromType(jsTypeInfo, receiverType, items, new HashSet<>()); addFieldsFromType(jsTypeInfo, receiverType, items, new HashSet<>()); - return; - } - - // For Java types with Java class, use reflection to get members - Class javaClass = receiverType.getJavaClass(); - if (javaClass != null) { - try { - // Add methods from Java reflection - for (java.lang.reflect.Method method : javaClass.getMethods()) { - String methodName = method.getName(); - StringBuilder signature = new StringBuilder(); - signature.append(methodName).append("("); - Class[] params = method.getParameterTypes(); - for (int i = 0; i < params.length; i++) { - if (i > 0) - signature.append(", "); - signature.append(params[i].getSimpleName()); - } - signature.append(")"); - - items.add(new AutocompleteItem.Builder() - .name(methodName) - .insertText(methodName) - .kind(AutocompleteItem.Kind.METHOD) - .typeLabel(method.getReturnType().getSimpleName()) - .signature(signature.toString()) - .build()); - } - - // Add fields from Java reflection - for (java.lang.reflect.Field field : javaClass.getFields()) { - items.add(new AutocompleteItem.Builder() - .name(field.getName()) - .insertText(field.getName()) - .kind(AutocompleteItem.Kind.FIELD) - .typeLabel(field.getType().getSimpleName()) - .build()); - } - } catch (SecurityException e) { - // Can't access members, skip - } } } From 176a3b6d50bf648cf486ba79555dd87ea4a4ae99 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 06:56:41 +0200 Subject: [PATCH 248/337] - Added Java.type("") for JS API - Added synthetic types (non-existant in java classloader and JS API, but custom defined like Nashorn's Java.type/extends/etc.. --- .../script/autocomplete/AutocompleteItem.java | 76 +++++ .../autocomplete/JSAutocompleteProvider.java | 29 +- .../script/interpreter/ScriptDocument.java | 83 +++++- .../script/interpreter/field/FieldInfo.java | 8 + .../interpreter/method/MethodCallInfo.java | 23 ++ .../script/interpreter/method/MethodInfo.java | 9 + .../interpreter/type/ClassTypeInfo.java | 73 +++++ .../interpreter/type/NashornBuiltins.java | 282 ++++++++++++++++++ .../script/interpreter/type/TypeInfo.java | 7 + .../script/interpreter/type/TypeResolver.java | 96 +++++- .../type/synthetic/SyntheticField.java | 33 ++ .../type/synthetic/SyntheticMethod.java | 136 +++++++++ .../type/synthetic/SyntheticParameter.java | 13 + .../type/synthetic/SyntheticType.java | 104 +++++++ .../type/synthetic/SyntheticTypeBuilder.java | 165 ++++++++++ 15 files changed, 1131 insertions(+), 6 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 8db4ea073..94b43f025 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -5,6 +5,9 @@ import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +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.SyntheticParameter; /** * Represents a single autocomplete suggestion. @@ -321,6 +324,79 @@ public static AutocompleteItem keyword(String keyword) { ); } + /** + * Create from a synthetic method (e.g., Nashorn built-in like Java.type). + */ + public static AutocompleteItem fromSyntheticMethod( + SyntheticMethod method, + TypeInfo containingType) { + String name = method.name; + StringBuilder insertText = new StringBuilder(name); + insertText.append("("); + insertText.append(")"); + + // Build display name with parameters - use simple names for types + StringBuilder displayName = new StringBuilder(name); + displayName.append("("); + for (int i = 0; i < method.parameters.size(); i++) { + if (i > 0) displayName.append(", "); + SyntheticParameter param = + method.parameters.get(i); + // Use simple name for display (e.g., "Class" instead of "java.lang.Class") + String simpleTypeName = getSimpleTypeName(param.typeName); + displayName.append(simpleTypeName).append(" ").append(param.name); + } + displayName.append(")"); + + // Use simple name for return type display too + String simpleReturnType = getSimpleTypeName(method.returnType); + + return new AutocompleteItem( + displayName.toString(), + name, // Search against just the method name + insertText.toString(), + Kind.METHOD, + simpleReturnType, + method.getSignature(), + method.documentation, + method, + false, + false, + null, + 0 // Synthetic types are at depth 0 + ); + } + + /** + * Get simple name from a fully-qualified type name. + */ + private static String getSimpleTypeName(String typeName) { + if (typeName == null) return "void"; + int lastDot = typeName.lastIndexOf('.'); + return lastDot >= 0 ? typeName.substring(lastDot + 1) : typeName; + } + + /** + * Create from a synthetic field (e.g., Nashorn built-in). + */ + public static AutocompleteItem fromSyntheticField( + SyntheticField field) { + return new AutocompleteItem( + field.name, + field.name, // searchName same as display name + field.name, + Kind.FIELD, + field.typeName, + field.typeName + " " + field.name, + field.documentation, + field, + false, + false, + null, + 0 // Synthetic types are at depth 0 + ); + } + // ==================== HELPER METHODS ==================== private static String buildMethodSignature(MethodInfo method) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index d0c85a3fe..641add7c0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -1,9 +1,7 @@ package noppes.npcs.client.gui.util.script.autocomplete; -import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; -import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.*; -import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.*; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -46,6 +44,14 @@ protected void addMemberSuggestions(Context context, List item return; } + // Check for synthetic types first (e.g., Nashorn Java object, custom types) + SyntheticType syntheticType = + document.getTypeResolver().getSyntheticType(receiverType.getSimpleName()); + if (syntheticType != null) { + addSyntheticTypeSuggestions(syntheticType, items); + return; + } + // If this is a Java type (has a Java class), delegate to parent's Java handling // This preserves static/instance context, modifiers, and all Java-specific behavior Class javaClass = receiverType.getJavaClass(); @@ -63,6 +69,23 @@ protected void addMemberSuggestions(Context context, List item } } + /** + * Add autocomplete suggestions for a synthetic type's methods and fields. + */ + private void addSyntheticTypeSuggestions(SyntheticType syntheticType, List items) { + // Add methods + for (SyntheticMethod method : syntheticType.getMethods()) { + AutocompleteItem item = AutocompleteItem.fromSyntheticMethod(method, syntheticType.getTypeInfo()); + items.add(item); + } + + // Add fields + for (SyntheticField field : syntheticType.getFields()) { + AutocompleteItem item = AutocompleteItem.fromSyntheticField(field); + items.add(item); + } + } + /** * Recursively add methods from a type and its parents. * @param type The current JS type to get methods from (changes as we walk up inheritance) 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 a96c0195c..6a1682f6b 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 @@ -20,6 +20,9 @@ import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; 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 java.util.*; import java.util.regex.Matcher; @@ -2661,7 +2664,39 @@ private void markMethodCalls(List marks) { MethodInfo resolvedMethod = null; if (receiverType != null) { - // Check for method existence using hierarchy search if it's a ScriptTypeInfo + // Check for synthetic types first (JavaScript only) + SyntheticType syntheticType = null; + if (isJavaScript()) { + syntheticType = typeResolver.getSyntheticType(receiverType.getSimpleName()); + } + + if (syntheticType != null && syntheticType.hasMethod(methodName)) { + // Synthetic type method call (e.g., Java.type()) + resolvedMethod = syntheticType.getMethodInfo(methodName); + + // Check for dynamic return type resolution (like Java.type("className")) + SyntheticMethod synMethod = syntheticType.getMethod(methodName); + TypeInfo dynamicReturnType = null; + if (synMethod != null && synMethod.returnTypeResolver != null) { + String argsText = text.substring(openParen + 1, closeParen); + String[] strArgs = TypeResolver.parseStringArguments(argsText); + dynamicReturnType = synMethod.returnTypeResolver.resolve(strArgs); + } + + MethodCallInfo callInfo = new MethodCallInfo(methodName, nameStart, nameEnd, openParen, + closeParen, arguments, receiverType, resolvedMethod, false); + + // Set the actual resolved return type for downstream type resolution + if (dynamicReturnType != null) { + callInfo.setResolvedReturnType(dynamicReturnType); + } + + callInfo.validate(); + methodCalls.add(callInfo); + marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, callInfo)); + continue; + } + // Regular type - check for method existence using hierarchy search if it's a ScriptTypeInfo boolean hasMethod = false; if (receiverType instanceof ScriptTypeInfo) { hasMethod = ((ScriptTypeInfo) receiverType).hasMethodInHierarchy(methodName); @@ -3496,6 +3531,42 @@ private TypeInfo resolveChainSegment(TypeInfo currentType, ChainSegment segment) } } + /** + * Resolve a chain segment on a synthetic type (like Nashorn's Java object). + */ + /** + * Resolve a chain segment for a synthetic type (method call or field access). + * Handles dynamic return type resolution for methods like Java.type(). + */ + private TypeInfo resolveSyntheticChainSegment(SyntheticType syntheticType, ChainSegment segment) { + if (segment.isMethodCall) { + SyntheticMethod method = syntheticType.getMethod(segment.name); + if (method != null) { + // For methods with dynamic return type resolvers (like Java.type), + // extract string arguments and resolve + if (method.returnTypeResolver != null && segment.arguments != null) { + String[] args = TypeResolver.parseStringArguments(segment.arguments); + TypeInfo resolved = method.returnTypeResolver.resolve(args); + if (resolved != null) { + return resolved; + } + } + // Fall back to static return type + TypeInfo returnType = typeResolver.resolve(method.returnType); + return returnType != null ? returnType : TypeInfo.unresolved(method.returnType, method.returnType); + } + } else { + SyntheticField field = syntheticType.getField(segment.name); + if (field != null) { + TypeInfo fieldType = typeResolver.resolve(field.typeName); + return fieldType != null ? fieldType : TypeInfo.unresolved(field.typeName, field.typeName); + } + } + return null; + } + + + // ==================== OPERATOR EXPRESSION RESOLUTION ==================== /** @@ -5023,6 +5094,16 @@ private void markImportedClassUsages(List marks) { if (isInImportOrPackage(start)) continue; + // For JavaScript, first check synthetic types (Nashorn built-ins like Java, print, etc.) + if (isJavaScript() && typeResolver.isSyntheticType(className)) { + SyntheticType syntheticType = typeResolver.getSyntheticType(className); + // Mark as IMPORTED_CLASS since it's a type reference + // Pass the TypeInfo (which the hover system can display) + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.IMPORTED_CLASS, + syntheticType.getTypeInfo())); + continue; + } + // Try to resolve the class TypeInfo info = resolveType(className); if (info != null && info.isResolved()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 9182cc267..ec3958f02 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -135,6 +135,14 @@ public static FieldInfo reflectionParam(String name, TypeInfo type) { return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1, 0, null); } + /** + * Create a FieldInfo for a synthetic/external field. + * Used for built-in types like Nashorn's Java object. + */ + public static FieldInfo external(String name, TypeInfo type, String documentation, int modifiers) { + return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null); + } + /** * Create a FieldInfo from reflection data for a class field. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index dfafad691..c6362d701 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -197,6 +197,29 @@ public void setExpectedType(TypeInfo expectedType) { this.expectedType = expectedType; } + // Dynamically resolved return type (e.g., for Java.type() which returns ClassTypeInfo) + private TypeInfo resolvedReturnType; + + /** + * Get the resolved return type of this method call. + * This may differ from resolvedMethod.getReturnType() for methods with dynamic return types. + * @return The resolved return type, or the method's declared return type if not dynamically resolved + */ + public TypeInfo getResolvedReturnType() { + if (resolvedReturnType != null) { + return resolvedReturnType; + } + return resolvedMethod != null ? resolvedMethod.getReturnType() : null; + } + + /** + * Set a dynamically resolved return type. + * Used for methods like Java.type() where the return type depends on the arguments. + */ + public void setResolvedReturnType(TypeInfo returnType) { + this.resolvedReturnType = returnType; + } + public boolean isConstructor() { return isConstructor; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 039127ea5..ababd9816 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -127,6 +127,15 @@ public static MethodInfo unresolvedCall(String name, int paramCount) { return new MethodInfo(name, null, null, params, -1, -1, -1, -1, -1, false, false, 0, null, null); } + /** + * Create a MethodInfo for a synthetic/external method. + * Used for built-in types like Nashorn's Java object. + */ + public static MethodInfo external(String name, TypeInfo returnType, TypeInfo containingType, + List params, int modifiers, String documentation) { + return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, null); + } + /** * Create a MethodInfo from reflection data. * Used when resolving method calls on known types. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java new file mode 100644 index 000000000..c19faeb2c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/ClassTypeInfo.java @@ -0,0 +1,73 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +/** + * Represents a reference to a Java Class itself (not an instance). + * Used for patterns like: var File = Java.type("java.io.File"); + * + * When you have a ClassTypeInfo: + * - Accessing members should show ONLY static members (File.listRoots()) + * - Using 'new' should return an instance of the wrapped class (new File("path")) + * + * This is distinct from a regular TypeInfo which represents an instance of that type. + */ +public class ClassTypeInfo extends TypeInfo { + + private final TypeInfo instanceType; // The type you get when you do 'new' on this class + + /** + * Create a ClassTypeInfo wrapping a Java class. + * + * @param javaClass The Java class this represents (e.g., java.io.File.class) + */ + public ClassTypeInfo(Class javaClass) { + super( + javaClass.getSimpleName(), + javaClass.getName(), + javaClass.getPackage() != null ? javaClass.getPackage().getName() : "", + javaClass.isInterface() ? Kind.INTERFACE : (javaClass.isEnum() ? Kind.ENUM : Kind.CLASS), + javaClass, + true, + null, + true // subclass marker + ); + this.instanceType = TypeInfo.fromClass(javaClass); + } + + /** + * Create a ClassTypeInfo from an existing TypeInfo. + */ + public ClassTypeInfo(TypeInfo instanceType) { + super( + instanceType.getSimpleName(), + instanceType.getFullName(), + instanceType.getPackageName(), + instanceType.getKind(), + instanceType.getJavaClass(), + instanceType.isResolved(), + instanceType.getEnclosingType(), + true // subclass marker + ); + this.instanceType = instanceType; + } + + /** + * Returns true - this TypeInfo represents a class reference, not an instance. + */ + @Override + public boolean isClassReference() { + return true; + } + + /** + * Get the TypeInfo for instances of this class. + * This is what you get when you call 'new' on this class. + */ + public TypeInfo getInstanceType() { + return instanceType; + } + + @Override + public String toString() { + return "Class<" + getSimpleName() + ">"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java new file mode 100644 index 000000000..3b425b5bb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java @@ -0,0 +1,282 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticParameter; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticTypeBuilder; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType; + +import java.util.*; + +/** + * Registry for Nashorn built-in types and global functions. + * + *

Nashorn provides several built-in objects and functions:

+ *
    + *
  • Java - Object for Java interop: type(), extend(), from(), to()
  • + *
  • Packages - Root of package hierarchy for accessing Java packages
  • + *
  • JavaImporter - Creates scope with imported Java packages
  • + *
  • print() - Global print function
  • + *
  • load() - Load and execute a script
  • + *
+ */ +public class NashornBuiltins { + + private static NashornBuiltins instance; + + private final Map builtinTypes = new LinkedHashMap<>(); + private final Map globalFunctions = new LinkedHashMap<>(); + + private NashornBuiltins() { + initializeBuiltins(); + } + + public static NashornBuiltins getInstance() { + if (instance == null) { + instance = new NashornBuiltins(); + } + return instance; + } + + private void initializeBuiltins() { + // ==================== Java object ==================== + SyntheticType javaType = new SyntheticTypeBuilder("Java") + .documentation("Nashorn's Java interop object for accessing Java types and utilities.") + + .addMethod("type") + .parameter("className", "String", "Fully-qualified Java class name") + .returns("java.lang.Class") + .returnsResolved(args -> { + if (args != null && args.length > 0 && args[0] != null) { + String className = args[0]; + // Remove quotes if present + if (className.startsWith("\"") && className.endsWith("\"")) { + className = className.substring(1, className.length() - 1); + } else if (className.startsWith("'") && className.endsWith("'")) { + className = className.substring(1, className.length() - 1); + } + // Try to load the class + TypeInfo resolved = TypeResolver.getInstance().resolve(className); + if (resolved != null && resolved.isResolved()) { + return new ClassTypeInfo(resolved); + } + } + return null; + }) + .documentation("Loads a Java class by its fully-qualified name.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var File = Java.type(\"java.io.File\");\n" + + "var file = new File(\"path/to/file\");\n" + + "```\n\n" + + "@param className The fully-qualified Java class name\n" + + "@returns The Java class reference (use with 'new' to create instances)") + .done() + + .addMethod("extend") + .parameter("type", "java.lang.Class", "Java class or interface to extend/implement") + .returns("java.lang.Class") + .documentation("Creates a subclass or implementation of a Java class or interface.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var MyRunnable = Java.extend(Java.type(\"java.lang.Runnable\"), {\n" + + " run: function() {\n" + + " print(\"Running!\");\n" + + " }\n" + + "});\n" + + "```\n\n" + + "@param type The Java class or interface to extend\n" + + "@returns A new class that extends/implements the given type") + .done() + + .addMethod("from") + .parameter("javaArray", "Object", "A Java array or Collection") + .returns("Array") + .documentation("Converts a Java array or Collection to a JavaScript array.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var jsList = Java.from(javaArrayList);\n" + + "jsList.forEach(function(item) { print(item); });\n" + + "```\n\n" + + "@param javaArray A Java array or java.util.Collection\n" + + "@returns A JavaScript array containing the elements") + .done() + + .addMethod("to") + .parameter("jsArray", "Array", "A JavaScript array") + .parameter("javaType", "java.lang.Class", "The target Java array type") + .returns("Object") + .documentation("Converts a JavaScript array to a Java array of the specified type.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var jsArray = [1, 2, 3];\n" + + "var intArray = Java.to(jsArray, \"int[]\");\n" + + "```\n\n" + + "@param jsArray A JavaScript array\n" + + "@param javaType The target Java array type (e.g., \"int[]\", \"java.lang.String[]\")\n" + + "@returns A Java array of the specified type") + .done() + + .addMethod("super") + .parameter("object", "Object", "A Java object created via Java.extend()") + .returns("Object") + .documentation("Gets a reference to the super class for calling super methods.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var MyList = Java.extend(Java.type(\"java.util.ArrayList\"), {\n" + + " add: function(e) {\n" + + " print(\"Adding: \" + e);\n" + + " return Java.super(this).add(e);\n" + + " }\n" + + "});\n" + + "```\n\n" + + "@param object An extended Java object\n" + + "@returns A reference to call super methods") + .done() + + .addMethod("synchronized") + .parameter("func", "Function", "The function to synchronize") + .parameter("lock", "Object", "The object to synchronize on") + .returns("Function") + .documentation("Wraps a function to execute synchronized on a given object.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "var syncFunc = Java.synchronized(function() {\n" + + " // Thread-safe code here\n" + + "}, lockObject);\n" + + "```\n\n" + + "@param func The function to synchronize\n" + + "@param lock The object to use as the monitor\n" + + "@returns A synchronized version of the function") + .done() + + .build(); + builtinTypes.put("Java", javaType); + + // ==================== Global print function ==================== + SyntheticType printFunc = new SyntheticTypeBuilder("print") + .addMethod("print") + .parameter("message", "Object", "The message to print") + .returns("void") + .documentation("Prints a message to standard output.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "print(\"Hello, world!\");\n" + + "print(myObject);\n" + + "```") + .done() + .build(); + globalFunctions.put("print", printFunc.getMethod("print")); + + // ==================== Global load function ==================== + SyntheticType loadFunc = new SyntheticTypeBuilder("load") + .addMethod("load") + .parameter("script", "String", "Path or URL to the script") + .returns("Object") + .documentation("Loads and executes a script file or URL.\n\n" + + "**Usage:**\n" + + "```javascript\n" + + "load(\"./myScript.js\");\n" + + "load(\"http://example.com/script.js\");\n" + + "```\n\n" + + "@param script Path to a local script or URL\n" + + "@returns The result of the script execution") + .done() + .build(); + globalFunctions.put("load", loadFunc.getMethod("load")); + } + + /** + * Get a built-in type by name. + * @param name The type name (e.g., "Java") + * @return The SyntheticType or null if not found + */ + public SyntheticType getBuiltinType(String name) { + return builtinTypes.get(name); + } + + /** + * Check if a name is a built-in type. + */ + public boolean isBuiltinType(String name) { + return builtinTypes.containsKey(name); + } + + /** + * Get a global function by name. + */ + public SyntheticMethod getGlobalFunction(String name) { + return globalFunctions.get(name); + } + + /** + * Check if a name is a global function. + */ + public boolean isGlobalFunction(String name) { + return globalFunctions.containsKey(name); + } + + /** + * Get all built-in type names. + */ + public Set getBuiltinTypeNames() { + return Collections.unmodifiableSet(builtinTypes.keySet()); + } + + /** + * Get all global function names. + */ + public Set getGlobalFunctionNames() { + return Collections.unmodifiableSet(globalFunctions.keySet()); + } + + /** + * Get all built-in types. + */ + public Collection getAllBuiltinTypes() { + return Collections.unmodifiableCollection(builtinTypes.values()); + } + + /** + * Get all global functions as SyntheticTypes. + * Each function is wrapped in its own SyntheticType for the registry. + */ + public Map getAllGlobalFunctions() { + Map result = new LinkedHashMap<>(); + + // Wrap each global function in a minimal SyntheticType + for (Map.Entry entry : globalFunctions.entrySet()) { + String name = entry.getKey(); + SyntheticMethod method = entry.getValue(); + + // Create a synthetic type that just contains this one global function + SyntheticTypeBuilder builder = new SyntheticTypeBuilder(name); + builder.documentation("Global " + name + " function"); + + // Add the method + SyntheticTypeBuilder.MethodBuilder methodBuilder = builder.addMethod(name); + for (SyntheticParameter param : method.parameters) { + methodBuilder.parameter(param.name, param.typeName, param.documentation); + } + methodBuilder.returns(method.returnType); + if (method.documentation != null && !method.documentation.isEmpty()) { + methodBuilder.documentation(method.documentation); + } + methodBuilder.done(); + + result.put(name, builder.build()); + } + + return result; + } + + /** + * Resolve the return type of Java.type() given the class name argument. + */ + public TypeInfo resolveJavaType(String className) { + SyntheticType javaBuiltin = builtinTypes.get("Java"); + if (javaBuiltin != null) { + return javaBuiltin.resolveMethodReturnType("type", new String[]{className}); + } + return null; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 5983012de..14f4c6242 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -240,6 +240,13 @@ public JSTypeInfo getJSTypeInfo() { public boolean isEnum() {return kind == Kind.ENUM;} public boolean isPrimitive() {return javaClass != null && javaClass.isPrimitive();} + /** + * Returns true if this TypeInfo represents a Class reference (not an instance). + * For example, Java.type("java.io.File") returns a Class reference. + * Override in ClassTypeInfo to return true. + */ + public boolean isClassReference() { return false; } + /** * Get the appropriate TokenType for highlighting this type. */ 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 81349ef52..4b930c934 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 @@ -1,11 +1,12 @@ package noppes.npcs.client.gui.util.script.interpreter.type; import noppes.npcs.client.gui.util.script.PackageFinder; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType; import java.util.*; @@ -29,6 +30,9 @@ public class TypeResolver { // JavaScript type registry (lazily initialized) private JSTypeRegistry jsTypeRegistry; + // Synthetic type registry (e.g., Nashorn built-ins) + private final Map syntheticTypes = new LinkedHashMap<>(); + // Auto-imported java.lang classes public static final Set JAVA_LANG_CLASSES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", @@ -70,6 +74,60 @@ public TypeResolver() { validPackages.add("java.lang"); validPackages.add("java.util"); validPackages.add("java.io"); + + // Initialize built-in synthetic types + initializeSyntheticTypes(); + } + + /** + * Initialize built-in synthetic types (e.g., Nashorn built-ins). + */ + private void initializeSyntheticTypes() { + // Register all Nashorn built-in types (Java, etc.) + for (SyntheticType type : NashornBuiltins.getInstance().getAllBuiltinTypes()) { + syntheticTypes.put(type.getName(), type); + } + + // Register all Nashorn global functions (print, load, etc.) + Map nashornGlobals = NashornBuiltins.getInstance().getAllGlobalFunctions(); + syntheticTypes.putAll(nashornGlobals); + } + + // ==================== SYNTHETIC TYPE REGISTRY ==================== + + /** + * Register a synthetic type for use in scripts. + * @param name The type name (e.g., "Java", "MyCustomType") + * @param type The synthetic type definition + */ + public void registerSyntheticType(String name, SyntheticType type) { + syntheticTypes.put(name, type); + } + + /** + * Check if a name is a known synthetic type. + * @param name The type name to check + * @return true if the name is a registered synthetic type + */ + public boolean isSyntheticType(String name) { + return syntheticTypes.containsKey(name); + } + + /** + * Get a synthetic type by name. + * @param name The type name + * @return The synthetic type, or null if not found + */ + public SyntheticType getSyntheticType(String name) { + return syntheticTypes.get(name); + } + + /** + * Get all registered synthetic types. + * @return Collection of all synthetic types + */ + public Collection getAllSyntheticTypes() { + return syntheticTypes.values(); } // ==================== JS TYPE REGISTRY ACCESS ==================== @@ -124,6 +182,34 @@ public void clearCache() { } // ==================== UNIFIED TYPE RESOLUTION ==================== + + /** + * General-purpose type resolution. + * Tries JS types first, then falls back to Java types. + * + * @param typeName The type name (can be simple or fully-qualified) + * @return TypeInfo for the resolved type, or null if not found + */ + public TypeInfo resolve(String typeName) { + if (typeName == null || typeName.isEmpty()) { + return null; + } + + // Try JS resolution first (handles primitives, .d.ts types) + TypeInfo jsType = resolveJSType(typeName); + if (jsType != null && jsType.isResolved()) { + return jsType; + } + + // Try full name resolution + TypeInfo fullType = resolveFullName(typeName); + if (fullType != null && fullType.isResolved()) { + return fullType; + } + + // Return unresolved or null + return jsType; // Will be unresolved TypeInfo or null + } /** * Resolve a type name for JavaScript context. @@ -148,6 +234,12 @@ public TypeInfo resolveJSType(String jsTypeName) { // Strip array brackets for base type resolution boolean isArray = jsTypeName.endsWith("[]"); String baseName = isArray ? jsTypeName.substring(0, jsTypeName.length() - 2) : jsTypeName; + + // Check synthetic types (Nashorn built-ins, custom types, etc.) + if (isSyntheticType(baseName)) { + SyntheticType syntheticType = getSyntheticType(baseName); + return syntheticType.getTypeInfo(); + } // Handle "Java." prefix - convert to actual Java type // e.g., "Java.java.io.File" -> "java.io.File" diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java new file mode 100644 index 000000000..1f798e4f6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java @@ -0,0 +1,33 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.lang.reflect.Modifier; + +public class SyntheticField { + public final String name; + public final String typeName; + public final String documentation; + public final boolean isStatic; + + SyntheticField(String name, String typeName, String documentation, boolean isStatic) { + this.name = name; + this.typeName = typeName; + this.documentation = documentation; + this.isStatic = isStatic; + } + + public FieldInfo toFieldInfo() { + TypeInfo type = TypeResolver.getInstance().resolve(typeName); + if (type == null) { + type = TypeInfo.unresolved(typeName, typeName); + } + int modifiers = Modifier.PUBLIC; + if (isStatic) { + modifiers |= Modifier.STATIC; + } + return FieldInfo.external(name, type, documentation, modifiers); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java new file mode 100644 index 000000000..6bd554904 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java @@ -0,0 +1,136 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SyntheticMethod { + public final String name; + public final String returnType; + public final List parameters; + public final String documentation; + public final boolean isStatic; + public final SyntheticTypeBuilder.ReturnTypeResolver returnTypeResolver; + + SyntheticMethod(String name, String returnType, List parameters, + String documentation, boolean isStatic, + SyntheticTypeBuilder.ReturnTypeResolver returnTypeResolver) { + this.name = name; + this.returnType = returnType; + this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters)); + this.documentation = documentation; + this.isStatic = isStatic; + this.returnTypeResolver = returnTypeResolver; + } + + /** + * Create a MethodInfo from this synthetic method. + */ + public MethodInfo toMethodInfo(TypeInfo containingType) { + List paramInfos = new ArrayList<>(); + for (SyntheticParameter param : parameters) { + TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); + if (paramType == null) { + paramType = TypeInfo.unresolved(param.typeName, param.typeName); + } + paramInfos.add(FieldInfo.parameter(param.name, paramType, -1, null)); + } + + TypeInfo returnTypeInfo = TypeResolver.getInstance().resolve(returnType); + if (returnTypeInfo == null) { + returnTypeInfo = TypeInfo.unresolved(returnType, returnType); + } + + int modifiers = Modifier.PUBLIC; + if (isStatic) { + modifiers |= Modifier.STATIC; + } + + MethodInfo methodInfo = MethodInfo.external(name, returnTypeInfo, containingType, paramInfos, modifiers, null); + + // Create JSDocInfo from documentation + if (documentation != null && !documentation.isEmpty()) { + JSDocInfo jsDocInfo = createJSDocInfo(returnTypeInfo); + methodInfo.setJSDocInfo(jsDocInfo); + } + + return methodInfo; + } + + /** + * Create JSDocInfo from the documentation string. + */ + private JSDocInfo createJSDocInfo(TypeInfo returnTypeInfo) { + // Parse the documentation to extract description and separate sections + String[] lines = documentation.split("\\n"); + StringBuilder descBuilder = new StringBuilder(); + + // Extract description (everything before @param or @returns) + for (String line : lines) { + line = line.trim(); + if (line.startsWith("@param") || line.startsWith("@returns") || line.startsWith("@return")) { + break; + } + if (!line.isEmpty()) { + if (descBuilder.length() > 0) + descBuilder.append("\n"); + descBuilder.append(line); + } + } + + JSDocInfo jsDocInfo = new JSDocInfo(documentation, -1, -1); + jsDocInfo.setDescription(descBuilder.toString()); + + // Add @param tags for each parameter + for (int i = 0; i < parameters.size(); i++) { + SyntheticParameter param = parameters.get(i); + TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); + if (paramType == null) { + paramType = TypeInfo.unresolved(param.typeName, param.typeName); + } + + JSDocParamTag paramTag = JSDocParamTag.create( + -1, -1, -1, // offsets + param.typeName, paramType, -1, -1, // type info + param.name, -1, -1, // param name + param.documentation // description + ); + jsDocInfo.addParamTag(paramTag); + } + + // Add @returns tag + JSDocReturnTag returnTag = JSDocReturnTag.create( + "returns", -1, -1, -1, // offsets + returnTypeInfo.getSimpleName(), returnTypeInfo, -1, -1, // type info + null // description extracted from main documentation + ); + jsDocInfo.setReturnTag(returnTag); + + return jsDocInfo; + } + + /** + * Get signature string for display. + */ + public String getSignature() { + StringBuilder sb = new StringBuilder(); + sb.append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) + sb.append(", "); + SyntheticParameter p = parameters.get(i); + sb.append(p.name).append(": ").append(p.typeName); + } + sb.append("): ").append(returnType); + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java new file mode 100644 index 000000000..9b58bebf3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java @@ -0,0 +1,13 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +public class SyntheticParameter { + public final String name; + public final String typeName; + public final String documentation; + + SyntheticParameter(String name, String typeName, String documentation) { + this.name = name; + this.typeName = typeName; + this.documentation = documentation; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java new file mode 100644 index 000000000..356891caf --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java @@ -0,0 +1,104 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a fully-built synthetic type. + */ +public class SyntheticType { + private final String name; + private final String documentation; + private final Map methods; + private final Map fields; + private TypeInfo typeInfo; + + SyntheticType(String name, String documentation, List methods, + List fields) { + this.name = name; + this.documentation = documentation; + this.methods = new LinkedHashMap<>(); + for (SyntheticMethod m : methods) { + this.methods.put(m.name, m); + } + this.fields = new LinkedHashMap<>(); + for (SyntheticField f : fields) { + this.fields.put(f.name, f); + } + } + + public String getName() { + return name; + } + + public String getDocumentation() { + return documentation; + } + + public SyntheticMethod getMethod(String methodName) { + return methods.get(methodName); + } + + public Collection getMethods() { + return methods.values(); + } + + public SyntheticField getField(String fieldName) { + return fields.get(fieldName); + } + + public Collection getFields() { + return fields.values(); + } + + public boolean hasMethod(String methodName) { + return methods.containsKey(methodName); + } + + public boolean hasField(String fieldName) { + return fields.containsKey(fieldName); + } + + /** + * Get or create the TypeInfo for this synthetic type. + */ + public TypeInfo getTypeInfo() { + if (typeInfo == null) { + typeInfo = TypeInfo.resolved(name, name, "", TypeInfo.Kind.CLASS, null); + } + return typeInfo; + } + + /** + * Create a MethodInfo for a method in this type. + */ + public MethodInfo getMethodInfo(String methodName) { + SyntheticMethod method = methods.get(methodName); + if (method == null) + return null; + return method.toMethodInfo(getTypeInfo()); + } + + /** + * Resolve the return type of a method given arguments. + * Used for special methods like Java.type(). + */ + public TypeInfo resolveMethodReturnType(String methodName, String[] arguments) { + SyntheticMethod method = methods.get(methodName); + if (method == null) + return null; + + if (method.returnTypeResolver != null) { + return method.returnTypeResolver.resolve(arguments); + } + + // Fall back to static return type + return TypeResolver.getInstance().resolve(method.returnType); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java new file mode 100644 index 000000000..02fc4d586 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticTypeBuilder.java @@ -0,0 +1,165 @@ +package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; + +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.*; + +/** + * Builder for creating synthetic (non-reflection-based) types. + * Used for built-in Nashorn objects like 'Java' that have no corresponding Java class. + * + *

Usage:

+ *
+ * SyntheticType javaType = new SyntheticTypeBuilder("Java")
+ *     .addMethod("type")
+ *         .parameter("className", "string")
+ *         .returns("Class")
+ *         .documentation("Loads a Java class by fully-qualified name.")
+ *         .done()
+ *     .build();
+ * 
+ */ +public class SyntheticTypeBuilder { + + private final String name; + private String documentation; + private final List methods = new ArrayList<>(); + private final List fields = new ArrayList<>(); + + public SyntheticTypeBuilder(String name) { + this.name = name; + } + + public SyntheticTypeBuilder documentation(String doc) { + this.documentation = doc; + return this; + } + + /** + * Start building a method for this type. + */ + public MethodBuilder addMethod(String methodName) { + return new MethodBuilder(this, methodName); + } + + /** + * Add a field to this type. + */ + public SyntheticTypeBuilder addField(String fieldName, String typeName, String doc) { + fields.add(new SyntheticField(fieldName, typeName, doc, false)); + return this; + } + + /** + * Add a static field to this type. + */ + public SyntheticTypeBuilder addStaticField(String fieldName, String typeName, String doc) { + fields.add(new SyntheticField(fieldName, typeName, doc, true)); + return this; + } + + void addBuiltMethod(SyntheticMethod method) { + methods.add(method); + } + + /** + * Build the synthetic type. + */ + public SyntheticType build() { + return new SyntheticType(name, documentation, methods, fields); + } + + // ==================== Inner classes ==================== + + /** + * Builder for methods within a synthetic type. + */ + public static class MethodBuilder { + private final SyntheticTypeBuilder parent; + private final String name; + private final List parameters = new ArrayList<>(); + private String returnType = "void"; + private String documentation; + private boolean isStatic = false; + private ReturnTypeResolver returnTypeResolver; + + MethodBuilder(SyntheticTypeBuilder parent, String name) { + this.parent = parent; + this.name = name; + } + + /** + * Add a parameter to this method. + */ + public MethodBuilder parameter(String paramName, String typeName) { + parameters.add(new SyntheticParameter(paramName, typeName, null)); + return this; + } + + /** + * Add a parameter with documentation. + */ + public MethodBuilder parameter(String paramName, String typeName, String doc) { + parameters.add(new SyntheticParameter(paramName, typeName, doc)); + return this; + } + + /** + * Set the return type. + */ + public MethodBuilder returns(String typeName) { + this.returnType = typeName; + return this; + } + + /** + * Set a dynamic return type resolver. + * Used for methods like Java.type() where return type depends on arguments. + */ + public MethodBuilder returnsResolved(ReturnTypeResolver resolver) { + this.returnTypeResolver = resolver; + return this; + } + + /** + * Set method documentation. + */ + public MethodBuilder documentation(String doc) { + this.documentation = doc; + return this; + } + + /** + * Mark this method as static. + */ + public MethodBuilder asStatic() { + this.isStatic = true; + return this; + } + + /** + * Finish building this method and return to the type builder. + */ + public SyntheticTypeBuilder done() { + parent.addBuiltMethod(new SyntheticMethod(name, returnType, parameters, documentation, isStatic, returnTypeResolver)); + return parent; + } + } + + /** + * Functional interface for resolving return types dynamically. + */ + @FunctionalInterface + public interface ReturnTypeResolver { + /** + * Resolve the return type given the arguments passed to the method. + * @param arguments The string arguments (for methods like Java.type("className")) + * @return The resolved TypeInfo, or null if cannot be resolved + */ + TypeInfo resolve(String[] arguments); + } + + // ==================== Data classes ==================== + + // ==================== SyntheticType (the result) ==================== +} From bfcdd3c47c33847b95aa92dfb2a85009c3ac650e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 8 Jan 2026 07:05:30 +0200 Subject: [PATCH 249/337] - Set vars assigned with type ClassTypeInfo (e.g.Java.type("")) as static access - Unified isStaticAccess checkers to TypeResolver.isStaticAccessExpression --- .../JavaAutocompleteProvider.java | 7 +- .../script/interpreter/FieldChainMarker.java | 9 +- .../script/interpreter/ScriptDocument.java | 35 ++++-- .../script/interpreter/type/TypeResolver.java | 113 ++++++++++++++++++ 4 files changed, 143 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index e60e874eb..9a871afbb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -343,12 +343,11 @@ protected ScriptTypeInfo findEnclosingType(int position) { /** * Check if the receiver expression represents static access (class type). - * Similar logic to FieldChainMarker.isStaticContext(). + * Returns true if the expression is a TYPE NAME (not a variable). */ protected boolean isStaticAccess(String receiverExpr, int position) { - // Try to resolve the receiver expression as a type - TypeInfo typeCheck = document.resolveType(receiverExpr); - return typeCheck != null && typeCheck.isResolved(); + // Use unified static access checker + return TypeResolver.isStaticAccessExpression(receiverExpr, position, document); } protected UsageTracker getUsageTracker() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java index 9a3e2d181..a94d81bc8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/FieldChainMarker.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; @@ -477,9 +478,9 @@ private boolean isStaticContext(ChainContext ctx) { return false; String previousSegment = ctx.chain.segments.get(ctx.currentIndex - 1); - - // Try to resolve the previous segment as a type - TypeInfo typeCheck = document.resolveType(previousSegment); - return typeCheck != null && typeCheck.isResolved(); + int prevPos = ctx.chain.positions.get(ctx.currentIndex - 1)[0]; + + // Use unified static access checker + return TypeResolver.isStaticAccessExpression(previousSegment, prevPos, document); } } 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 6a1682f6b..41378ea25 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 @@ -2789,6 +2789,10 @@ private void markMethodCalls(List marks) { * Check if a method call is a static access (Class.method() style). * Returns true if the immediate receiver before the dot is a class name (uppercase). */ + /** + * Check if a method call at the given position is static access. + * Walks backward from the method name to analyze the receiver. + */ private boolean isStaticAccessCall(int methodNameStart) { // Walk backward to find the dot int pos = methodNameStart - 1; @@ -2813,7 +2817,8 @@ private boolean isStaticAccessCall(int methodNameStart) { char c = text.charAt(pos); if (c == ')' || c == ']') { - // Method call or array - this is instance access + // Method call or array - would need complex expression resolution + // For now, conservatively treat as instance access return false; } @@ -2828,24 +2833,20 @@ private boolean isStaticAccessCall(int methodNameStart) { String ident = text.substring(identStart, identEnd); - // Check if this is a class name (starts with uppercase) - // AND check if there's no dot before it (making it a direct class reference) - // If there's a chain like "obj.SomeClass.method()", we need to check further - // Skip whitespace before the identifier while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--; - - // If preceded by a dot, this might be part of a longer chain - // In that case, it's not a direct static access + + // If preceded by a dot, this is part of a chain - treat as instance for now if (pos >= 0 && text.charAt(pos) == '.') { return false; } - - // It's static access if the identifier resolves to a type + + // Direct identifier - check if it resolves to a type (static) or variable (instance) if (ident.isEmpty()) return false; - TypeInfo typeCheck = resolveType(ident); - return typeCheck != null && typeCheck.isResolved(); + + // Use unified static access checker + return TypeResolver.isStaticAccessExpression(ident, identStart, this); } /** @@ -3486,6 +3487,14 @@ private TypeInfo resolveChainSegment(TypeInfo currentType, ChainSegment segment) if (currentType == null || !currentType.isResolved()) { return null; } + + // Check if current type is a synthetic type + if (isJavaScript()) { + SyntheticType syntheticType = typeResolver.getSyntheticType(currentType.getSimpleName()); + if (syntheticType != null) { + return resolveSyntheticChainSegment(syntheticType, segment); + } + } if (segment.isMethodCall) { // Method call - get return type with argument-based overload resolution @@ -5323,7 +5332,7 @@ FieldAccessInfo createFieldAccessInfo(String name, int start, int end, * Resolve a variable by name at a given position. * Works for BOTH Java and JavaScript using unified data structures. */ - FieldInfo resolveVariable(String name, int position) { + public FieldInfo resolveVariable(String name, int position) { // Find containing method/function MethodInfo containingMethod = findMethodAtPosition(position); 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 4b930c934..7b2673a05 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 @@ -668,4 +668,117 @@ public TokenType getTokenType() { return typeInfo.getTokenType(); } } + + /** + * Check if an expression represents static access (accessing a Class, not an instance). + * This checks if the expression STRING resolves to a TYPE name. + * + * Examples: + * - "String" -> true (it's a class name) + * - "myVar" -> false (it's a variable, even if myVar holds a ClassTypeInfo) + * - "Java" -> true (it's a synthetic type) + * + * Note: This is different from checking if a variable's TYPE is ClassTypeInfo. + * For variables holding ClassTypeInfo (like `var File = Java.type("File")`), + * the receiver type itself should be checked separately. + * + * @param typeInfo The resolved TypeInfo (can be null) + * @param wasResolvedAsType true if this was resolved as a type name (not a variable) + * @return true if this represents static access (direct class reference) + */ + public static boolean isStaticAccess(TypeInfo typeInfo, boolean wasResolvedAsType) { + // If explicitly resolved as a type name, it's static access + if (wasResolvedAsType) + return true; + + if (typeInfo == null) + return false; + + // If the typeInfo itself is a ClassTypeInfo (e.g., result of Java.type()), + // then accessing members on it should be static + if (typeInfo.isClassReference()) + return true; + + return false; + } + + /** + * Unified static access checker for expressions. + * Determines if an identifier represents static access (type name) or instance access (variable). + * + * This method is used by ScriptDocument, JavaAutocompleteProvider, and FieldChainMarker. + * + * @param identifier The identifier to check (e.g., "String", "myVar", "Java") + * @param position The position in the document for context + * @param document The script document for resolution + * @return true if this represents static access + */ + public static boolean isStaticAccessExpression(String identifier, int position, ScriptDocument document) { + if (identifier == null || identifier.isEmpty() || document == null) { + return false; + } + + // First check if it's a variable/field (instance access) + FieldInfo fieldInfo = document.resolveVariable(identifier, position); + if (fieldInfo != null && fieldInfo.isResolved()) { + // It's a variable - check if its type is a ClassTypeInfo + return isStaticAccess(fieldInfo.getTypeInfo(), false); + } + + // Not a variable - check if it's a type name + TypeInfo typeCheck = document.resolveExpressionType(identifier,position); + boolean isType = typeCheck != null && typeCheck.isResolved(); + return isStaticAccess(typeCheck, isType); + } + + /** + * Parse string literal arguments from a method call. + * Used for resolving dynamic return types like Java.type("className"). + * + * @param argumentsStr The arguments string (e.g., "\"java.io.File\", 123") + * @return Array of parsed arguments + */ + public static String[] parseStringArguments(String argumentsStr) { + if (argumentsStr == null || argumentsStr.isEmpty()) { + return new String[0]; + } + + List args = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + int depth = 0; + boolean inString = false; + char stringChar = 0; + + for (int i = 0; i < argumentsStr.length(); i++) { + char c = argumentsStr.charAt(i); + + if (inString) { + current.append(c); + if (c == stringChar && (i == 0 || argumentsStr.charAt(i - 1) != '\\')) { + inString = false; + } + } else if (c == '"' || c == '\'') { + current.append(c); + inString = true; + stringChar = c; + } else if (c == '(' || c == '[' || c == '{') { + depth++; + current.append(c); + } else if (c == ')' || c == ']' || c == '}') { + depth--; + current.append(c); + } else if (c == ',' && depth == 0) { + args.add(current.toString().trim()); + current = new StringBuilder(); + } else { + current.append(c); + } + } + + if (current.length() > 0) { + args.add(current.toString().trim()); + } + + return args.toArray(new String[0]); + } } From 277fe3387d58f194adecea3417b6ea93e3a01092 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 9 Jan 2026 03:00:32 +0200 Subject: [PATCH 250/337] oops forgot important class --- .../type/SyntheticTypeBuilder.java | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java new file mode 100644 index 000000000..215b28bcd --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java @@ -0,0 +1,417 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; + +import java.lang.reflect.Modifier; +import java.util.*; + +/** + * Builder for creating synthetic (non-reflection-based) types. + * Used for built-in Nashorn objects like 'Java' that have no corresponding Java class. + * + *

Usage:

+ *
+ * SyntheticType javaType = new SyntheticTypeBuilder("Java")
+ *     .addMethod("type")
+ *         .parameter("className", "string")
+ *         .returns("Class")
+ *         .documentation("Loads a Java class by fully-qualified name.")
+ *         .done()
+ *     .build();
+ * 
+ */ +public class SyntheticTypeBuilder { + + private final String name; + private String documentation; + private final List methods = new ArrayList<>(); + private final List fields = new ArrayList<>(); + + public SyntheticTypeBuilder(String name) { + this.name = name; + } + + public SyntheticTypeBuilder documentation(String doc) { + this.documentation = doc; + return this; + } + + /** + * Start building a method for this type. + */ + public MethodBuilder addMethod(String methodName) { + return new MethodBuilder(this, methodName); + } + + /** + * Add a field to this type. + */ + public SyntheticTypeBuilder addField(String fieldName, String typeName, String doc) { + fields.add(new SyntheticField(fieldName, typeName, doc, false)); + return this; + } + + /** + * Add a static field to this type. + */ + public SyntheticTypeBuilder addStaticField(String fieldName, String typeName, String doc) { + fields.add(new SyntheticField(fieldName, typeName, doc, true)); + return this; + } + + void addBuiltMethod(SyntheticMethod method) { + methods.add(method); + } + + /** + * Build the synthetic type. + */ + public SyntheticType build() { + return new SyntheticType(name, documentation, methods, fields); + } + + // ==================== Inner classes ==================== + + /** + * Builder for methods within a synthetic type. + */ + public static class MethodBuilder { + private final SyntheticTypeBuilder parent; + private final String name; + private final List parameters = new ArrayList<>(); + private String returnType = "void"; + private String documentation; + private boolean isStatic = false; + private ReturnTypeResolver returnTypeResolver; + + MethodBuilder(SyntheticTypeBuilder parent, String name) { + this.parent = parent; + this.name = name; + } + + /** + * Add a parameter to this method. + */ + public MethodBuilder parameter(String paramName, String typeName) { + parameters.add(new SyntheticParameter(paramName, typeName, null)); + return this; + } + + /** + * Add a parameter with documentation. + */ + public MethodBuilder parameter(String paramName, String typeName, String doc) { + parameters.add(new SyntheticParameter(paramName, typeName, doc)); + return this; + } + + /** + * Set the return type. + */ + public MethodBuilder returns(String typeName) { + this.returnType = typeName; + return this; + } + + /** + * Set a dynamic return type resolver. + * Used for methods like Java.type() where return type depends on arguments. + */ + public MethodBuilder returnsResolved(ReturnTypeResolver resolver) { + this.returnTypeResolver = resolver; + return this; + } + + /** + * Set method documentation. + */ + public MethodBuilder documentation(String doc) { + this.documentation = doc; + return this; + } + + /** + * Mark this method as static. + */ + public MethodBuilder asStatic() { + this.isStatic = true; + return this; + } + + /** + * Finish building this method and return to the type builder. + */ + public SyntheticTypeBuilder done() { + parent.addBuiltMethod(new SyntheticMethod(name, returnType, parameters, documentation, isStatic, returnTypeResolver)); + return parent; + } + } + + /** + * Functional interface for resolving return types dynamically. + */ + @FunctionalInterface + public interface ReturnTypeResolver { + /** + * Resolve the return type given the arguments passed to the method. + * @param arguments The string arguments (for methods like Java.type("className")) + * @return The resolved TypeInfo, or null if cannot be resolved + */ + TypeInfo resolve(String[] arguments); + } + + // ==================== Data classes ==================== + + public static class SyntheticMethod { + public final String name; + public final String returnType; + public final List parameters; + public final String documentation; + public final boolean isStatic; + public final ReturnTypeResolver returnTypeResolver; + + SyntheticMethod(String name, String returnType, List parameters, + String documentation, boolean isStatic, ReturnTypeResolver returnTypeResolver) { + this.name = name; + this.returnType = returnType; + this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters)); + this.documentation = documentation; + this.isStatic = isStatic; + this.returnTypeResolver = returnTypeResolver; + } + + /** + * Create a MethodInfo from this synthetic method. + */ + public MethodInfo toMethodInfo(TypeInfo containingType) { + List paramInfos = new ArrayList<>(); + for (SyntheticParameter param : parameters) { + TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); + if (paramType == null) { + paramType = TypeInfo.unresolved(param.typeName, param.typeName); + } + paramInfos.add(FieldInfo.parameter(param.name, paramType, -1, null)); + } + + TypeInfo returnTypeInfo = TypeResolver.getInstance().resolve(returnType); + if (returnTypeInfo == null) { + returnTypeInfo = TypeInfo.unresolved(returnType, returnType); + } + + int modifiers = Modifier.PUBLIC; + if (isStatic) { + modifiers |= Modifier.STATIC; + } + + MethodInfo methodInfo = MethodInfo.external(name, returnTypeInfo, containingType, paramInfos, modifiers, null); + + // Create JSDocInfo from documentation + if (documentation != null && !documentation.isEmpty()) { + JSDocInfo jsDocInfo = createJSDocInfo(returnTypeInfo); + methodInfo.setJSDocInfo(jsDocInfo); + } + + return methodInfo; + } + + /** + * Create JSDocInfo from the documentation string. + */ + private JSDocInfo createJSDocInfo(TypeInfo returnTypeInfo) { + // Parse the documentation to extract description and separate sections + String[] lines = documentation.split("\n"); // Split on actual newlines, not \\n + StringBuilder descBuilder = new StringBuilder(); + + // Extract description (everything before @param or @returns) + for (String line : lines) { + line = line.trim(); + if (line.startsWith("@param") || line.startsWith("@returns") || line.startsWith("@return")) { + break; + } + if (!line.isEmpty()) { + if (descBuilder.length() > 0) descBuilder.append("\n"); // Use actual newline + descBuilder.append(line); + } + } + + JSDocInfo jsDocInfo = new JSDocInfo(documentation, -1, -1); + jsDocInfo.setDescription(descBuilder.toString()); + + // Add @param tags for each parameter + for (int i = 0; i < parameters.size(); i++) { + SyntheticParameter param = parameters.get(i); + TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); + if (paramType == null) { + paramType = TypeInfo.unresolved(param.typeName, param.typeName); + } + + JSDocParamTag paramTag = JSDocParamTag.create( + -1, -1, -1, // offsets + param.typeName, paramType, -1, -1, // type info + param.name, -1, -1, // param name + param.documentation // description + ); + jsDocInfo.addParamTag(paramTag); + } + + // Add @returns tag - use simple name for display but full TypeInfo for resolution + String displayTypeName = returnTypeInfo != null && returnTypeInfo.isResolved() + ? returnTypeInfo.getSimpleName() + : returnType; + + JSDocReturnTag returnTag = JSDocReturnTag.create( + "returns", -1, -1, -1, // offsets + displayTypeName, returnTypeInfo, -1, -1, // type name for display, TypeInfo for resolution + null // description extracted from main documentation + ); + jsDocInfo.setReturnTag(returnTag); + + return jsDocInfo; + } + + /** + * Get signature string for display. + */ + public String getSignature() { + StringBuilder sb = new StringBuilder(); + sb.append(name).append("("); + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) sb.append(", "); + SyntheticParameter p = parameters.get(i); + sb.append(p.name).append(": ").append(p.typeName); + } + sb.append("): ").append(returnType); + return sb.toString(); + } + } + + public static class SyntheticParameter { + public final String name; + public final String typeName; + public final String documentation; + + SyntheticParameter(String name, String typeName, String documentation) { + this.name = name; + this.typeName = typeName; + this.documentation = documentation; + } + } + + public static class SyntheticField { + public final String name; + public final String typeName; + public final String documentation; + public final boolean isStatic; + + SyntheticField(String name, String typeName, String documentation, boolean isStatic) { + this.name = name; + this.typeName = typeName; + this.documentation = documentation; + this.isStatic = isStatic; + } + + public FieldInfo toFieldInfo() { + TypeInfo type = TypeResolver.getInstance().resolve(typeName); + if (type == null) { + type = TypeInfo.unresolved(typeName, typeName); + } + int modifiers = Modifier.PUBLIC; + if (isStatic) { + modifiers |= Modifier.STATIC; + } + return FieldInfo.external(name, type, documentation, modifiers); + } + } + + // ==================== SyntheticType (the result) ==================== + + /** + * Represents a fully-built synthetic type. + */ + public static class SyntheticType { + private final String name; + private final String documentation; + private final Map methods; + private final Map fields; + private TypeInfo typeInfo; + + SyntheticType(String name, String documentation, List methods, List fields) { + this.name = name; + this.documentation = documentation; + this.methods = new LinkedHashMap<>(); + for (SyntheticMethod m : methods) { + this.methods.put(m.name, m); + } + this.fields = new LinkedHashMap<>(); + for (SyntheticField f : fields) { + this.fields.put(f.name, f); + } + } + + public String getName() { return name; } + public String getDocumentation() { return documentation; } + + public SyntheticMethod getMethod(String methodName) { + return methods.get(methodName); + } + + public Collection getMethods() { + return methods.values(); + } + + public SyntheticField getField(String fieldName) { + return fields.get(fieldName); + } + + public Collection getFields() { + return fields.values(); + } + + public boolean hasMethod(String methodName) { + return methods.containsKey(methodName); + } + + public boolean hasField(String fieldName) { + return fields.containsKey(fieldName); + } + + /** + * Get or create the TypeInfo for this synthetic type. + */ + public TypeInfo getTypeInfo() { + if (typeInfo == null) { + typeInfo = TypeInfo.resolved(name, name, "", TypeInfo.Kind.CLASS, null); + } + return typeInfo; + } + + /** + * Create a MethodInfo for a method in this type. + */ + public MethodInfo getMethodInfo(String methodName) { + SyntheticMethod method = methods.get(methodName); + if (method == null) return null; + return method.toMethodInfo(getTypeInfo()); + } + + /** + * Resolve the return type of a method given arguments. + * Used for special methods like Java.type(). + */ + public TypeInfo resolveMethodReturnType(String methodName, String[] arguments) { + SyntheticMethod method = methods.get(methodName); + if (method == null) return null; + + if (method.returnTypeResolver != null) { + return method.returnTypeResolver.resolve(arguments); + } + + // Fall back to static return type + return TypeResolver.getInstance().resolve(method.returnType); + } + } +} From c661bc95c87b51e1686b0072af3176bb60fb0bdd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 9 Jan 2026 04:28:58 +0200 Subject: [PATCH 251/337] Small AutocompleteMenu draw bug --- .../gui/util/script/autocomplete/AutocompleteMenu.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index e8dea011d..bfe84b16b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -53,6 +53,7 @@ public class AutocompleteMenu extends Gui { private int x, y; private int width, height; private int menuWidth; + private int visibleItemsCount; // Actual number of items that fit in the menu // ==================== FONT ==================== private final FontRenderer font; @@ -177,6 +178,9 @@ private void calculateDimensions(int cursorX, int cursorY, int viewportWidth, in } } + // Store the calculated visible items count + this.visibleItemsCount = visibleItems; + // Position the menu if (useHorizontalPosition) { // Position to the right of the cursor @@ -304,9 +308,8 @@ public void draw(int mouseX, int mouseY) { // Draw items int itemY = y + PADDING; - int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS); - for (int i = 0; i < visibleItems; i++) { + for (int i = 0; i < visibleItemsCount; i++) { int itemIndex = scrollOffset + i; if (itemIndex >= items.size()) break; @@ -319,7 +322,7 @@ public void draw(int mouseX, int mouseY) { } // Draw scrollbar if needed - if (items.size() > MAX_VISIBLE_ITEMS) { + if (items.size() > visibleItemsCount) { drawScrollbar(mouseX, mouseY); } From 5c604a6919a1c05b3d9433420404f2b9e3579cde Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 9 Jan 2026 04:46:47 +0200 Subject: [PATCH 252/337] Colored return type of members in Autocomplete menu according to their type token (Class/Interface/Enum/Primitive) --- .../script/autocomplete/AutocompleteItem.java | 17 ++++++++++++++--- .../script/autocomplete/AutocompleteMenu.java | 6 +++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 94b43f025..f4e761bc4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -44,6 +44,7 @@ public int getPriority() { private final String insertText; // Text to insert (e.g., "getPlayer()") private final Kind kind; // Type of completion private final String typeLabel; // Type label (e.g., "IPlayer", "void") + private final TypeInfo typeInfo; // TypeInfo for the typeLabel private final String signature; // Full signature for methods private final String documentation; // Documentation/description private final Object sourceData; // Original source (MethodInfo, FieldInfo, etc.) @@ -61,13 +62,14 @@ public int getPriority() { private int[] matchIndices; // Indices of matched characters for highlighting private AutocompleteItem(String name, String searchName, String insertText, Kind kind, String typeLabel, - String signature, String documentation, Object sourceData, boolean deprecated, - boolean requiresImport, String importPath, int inheritanceDepth) { + TypeInfo typeLabelTypeInfo, String signature, String documentation, Object sourceData, + boolean deprecated, boolean requiresImport, String importPath, int inheritanceDepth) { this.name = name; this.searchName = searchName != null ? searchName : name; // Default to name if not provided this.insertText = insertText; this.kind = kind; this.typeLabel = typeLabel; + this.typeInfo = typeLabelTypeInfo; this.signature = signature; this.documentation = documentation; this.sourceData = sourceData; @@ -119,6 +121,7 @@ public static AutocompleteItem fromMethod(MethodInfo method) { insertText.toString(), Kind.METHOD, returnType, + method.getReturnType(), // TypeInfo for return type signature, method.getDocumentation(), method, @@ -157,6 +160,7 @@ public static AutocompleteItem fromField(FieldInfo field) { field.getName(), kind, typeLabel, + field.getTypeInfo(), // TypeInfo for field type typeLabel + " " + field.getName(), null, field, @@ -189,6 +193,7 @@ public static AutocompleteItem fromType(TypeInfo type) { name, kind, type.getPackageName(), + type, // TypeInfo for package name type.getFullName(), null, type, @@ -251,6 +256,7 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contex insertText.toString(), Kind.METHOD, returnType, + returnTypeInfo, // TypeInfo for return type method.getSignature(), method.getDocumentation(), method, @@ -294,6 +300,7 @@ public static AutocompleteItem fromJSField(JSFieldInfo field, TypeInfo contextTy field.getName(), Kind.FIELD, fieldType, + fieldTypeInfo, // TypeInfo for field type field.toString(), field.getDocumentation(), field, @@ -314,6 +321,7 @@ public static AutocompleteItem keyword(String keyword) { keyword, Kind.KEYWORD, "keyword", + null, // TypeInfo for keyword null, null, null, @@ -357,6 +365,7 @@ public static AutocompleteItem fromSyntheticMethod( insertText.toString(), Kind.METHOD, simpleReturnType, + null, // TypeInfo for return type - would need to resolve from TypeResolver method.getSignature(), method.documentation, method, @@ -387,6 +396,7 @@ public static AutocompleteItem fromSyntheticField( field.name, Kind.FIELD, field.typeName, + null, // TypeInfo for field type - would need to resolve from TypeResolver field.typeName + " " + field.name, field.documentation, field, @@ -529,6 +539,7 @@ public void addScoreBoost(int boost) { public String getInsertText() { return insertText; } public Kind getKind() { return kind; } public String getTypeLabel() { return typeLabel; } + public TypeInfo getTypeInfo() { return typeInfo; } public String getSignature() { return signature; } public String getDocumentation() { return documentation; } public Object getSourceData() { return sourceData; } @@ -759,7 +770,7 @@ public AutocompleteItem build() { insertText = name; } return new AutocompleteItem(name, searchName, insertText, kind, typeLabel, - signature, documentation, sourceData, deprecated, requiresImport, importPath, -1); + null, signature, documentation, sourceData, deprecated, requiresImport, importPath, -1); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index bfe84b16b..5fede61bf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -5,6 +5,7 @@ import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.ScaledResolution; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import org.lwjgl.opengl.GL11; import java.util.ArrayList; @@ -400,7 +401,10 @@ private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth String typeLabel = item.getTypeLabel(); int typeLabelWidth = font.getStringWidth(typeLabel); int typeLabelX = itemX + itemWidth - typeLabelWidth - PADDING; - font.drawString(typeLabel, typeLabelX, textY, DIM_TEXT_COLOR); + + TypeInfo type = item.getTypeInfo(); + int col = type != null ? type.getTokenType().getHexColor() : DIM_TEXT_COLOR; + font.drawString(typeLabel, typeLabelX, textY, col); } } From 1e844444cfe6c70e6374b0587552d729cab139a8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 9 Jan 2026 05:11:07 +0200 Subject: [PATCH 253/337] Colored Autocomplete items according to their token types --- .../script/autocomplete/AutocompleteMenu.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java index 5fede61bf..52b8fd86f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteMenu.java @@ -378,19 +378,10 @@ private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth } // Determine text color - gray for inherited Object methods - int textColor; - if (item.isInheritedObjectMethod()) { - textColor = DIM_TEXT_COLOR; - } else if (item.isDeprecated()) { - textColor = DIM_TEXT_COLOR; - } else { - textColor = TEXT_COLOR; - } - - // Draw name with match highlighting and parameter coloring for methods + int textColor = getColor(item); if (item.getKind() == AutocompleteItem.Kind.METHOD) { - drawMethodNameTruncated(item.getName(), item.getMatchIndices(), textX, textY, - textColor, availableWidth); + drawMethodNameTruncated(item.getName(), item.getMatchIndices(), textX, textY, + textColor, availableWidth); } else { drawHighlightedTextTruncated(item.getName(), item.getMatchIndices(), textX, textY, textColor, availableWidth); @@ -407,6 +398,28 @@ private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth font.drawString(typeLabel, typeLabelX, textY, col); } } + + public int getColor(AutocompleteItem item) { + if (item.isInheritedObjectMethod() || item.isDeprecated()) + return DIM_TEXT_COLOR; + + switch (item.getKind()) { + case METHOD: + return TokenType.METHOD_CALL.getHexColor(); + case FIELD: + return TokenType.GLOBAL_FIELD.getHexColor(); + case ENUM_CONSTANT: + return TokenType.ENUM_CONSTANT.getHexColor(); + case CLASS: + return TokenType.getColor(item.getTypeInfo()); + case VARIABLE: + return TokenType.LOCAL_FIELD.getHexColor(); + case KEYWORD: + return TokenType.KEYWORD.getHexColor(); + default: + return TEXT_COLOR; + } + } /** * Draw method name with parameters colored gray. From bd0e8ad9077b19fae327264872a23dfb9f17b352 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 00:32:20 +0200 Subject: [PATCH 254/337] gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5708af3ec..f24532312 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ addon.late.local.gradle.kts layout.json out run +src/main/resources/assets/customnpcs/api \ No newline at end of file From c2435b350c5bd1afbb31da38bfc7c1da20b7d4d0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 00:32:55 +0200 Subject: [PATCH 255/337] Removed .d.s files for clean merge sake --- .../customnpcs/api/forge-events-raw.d.ts | 697 ------------------ .../assets/customnpcs/api/hooks.d.ts | 259 ------- .../assets/customnpcs/api/index.d.ts | 303 -------- .../assets/customnpcs/api/minecraft-raw.d.ts | 278 ------- .../minecraft/block/properties/IProperty.d.ts | 18 - .../minecraft/block/state/IBlockState.d.ts | 16 - .../api/net/minecraft/util/Vec3i.d.ts | 32 - .../api/net/minecraft/util/math/BlockPos.d.ts | 82 --- .../net/minecraft/util/math/IntHashMap.d.ts | 75 -- .../api/noppes/npcs/api/AbstractNpcAPI.d.ts | 123 ---- .../api/noppes/npcs/api/IBlock.d.ts | 42 -- .../api/noppes/npcs/api/ICommand.d.ts | 20 - .../api/noppes/npcs/api/IContainer.d.ts | 20 - .../api/noppes/npcs/api/IDamageSource.d.ts | 16 - .../customnpcs/api/noppes/npcs/api/INbt.d.ts | 43 -- .../api/noppes/npcs/api/IParticle.d.ts | 86 --- .../noppes/npcs/api/IPixelmonPlayerData.d.ts | 12 - .../customnpcs/api/noppes/npcs/api/IPos.d.ts | 44 -- .../api/noppes/npcs/api/IScreenSize.d.ts | 14 - .../api/noppes/npcs/api/ISkinOverlay.d.ts | 30 - .../api/noppes/npcs/api/ITileEntity.d.ts | 24 - .../api/noppes/npcs/api/ITimers.d.ts | 24 - .../api/noppes/npcs/api/IWorld.d.ts | 139 ---- .../noppes/npcs/api/block/IBlockScripted.d.ts | 55 -- .../api/noppes/npcs/api/block/ITextPlane.d.ts | 26 - .../api/noppes/npcs/api/entity/IAnimal.d.ts | 17 - .../noppes/npcs/api/entity/IAnimatable.d.ts | 11 - .../api/noppes/npcs/api/entity/IArrow.d.ts | 13 - .../noppes/npcs/api/entity/ICustomNpc.d.ts | 217 ------ .../noppes/npcs/api/entity/IDBCPlayer.d.ts | 71 -- .../api/noppes/npcs/api/entity/IEntity.d.ts | 101 --- .../noppes/npcs/api/entity/IEntityItem.d.ts | 22 - .../noppes/npcs/api/entity/IEntityLiving.d.ts | 33 - .../npcs/api/entity/IEntityLivingBase.d.ts | 65 -- .../api/noppes/npcs/api/entity/IFishHook.d.ts | 12 - .../api/noppes/npcs/api/entity/IMonster.d.ts | 8 - .../api/noppes/npcs/api/entity/IPixelmon.d.ts | 33 - .../api/noppes/npcs/api/entity/IPlayer.d.ts | 106 --- .../noppes/npcs/api/entity/IProjectile.d.ts | 21 - .../noppes/npcs/api/entity/IThrowable.d.ts | 12 - .../api/noppes/npcs/api/entity/IVillager.d.ts | 13 - .../noppes/npcs/api/entity/data/IMark.d.ts | 16 - .../npcs/api/entity/data/IModelData.d.ts | 30 - .../npcs/api/entity/data/IModelRotate.d.ts | 17 - .../api/entity/data/IModelRotatePart.d.ts | 16 - .../npcs/api/entity/data/IModelScale.d.ts | 11 - .../npcs/api/entity/data/IModelScalePart.d.ts | 14 - .../npcs/api/event/IAnimationEvent.d.ts | 27 - .../noppes/npcs/api/event/IBlockEvent.d.ts | 51 -- .../npcs/api/event/ICustomGuiEvent.d.ts | 32 - .../npcs/api/event/ICustomNPCsEvent.d.ts | 29 - .../noppes/npcs/api/event/IDialogEvent.d.ts | 19 - .../noppes/npcs/api/event/IFactionEvent.d.ts | 17 - .../noppes/npcs/api/event/IForgeEvent.d.ts | 19 - .../api/noppes/npcs/api/event/IItemEvent.d.ts | 66 -- .../npcs/api/event/ILinkedItemEvent.d.ts | 16 - .../api/noppes/npcs/api/event/INpcEvent.d.ts | 79 -- .../noppes/npcs/api/event/IPartyEvent.d.ts | 34 - .../noppes/npcs/api/event/IPlayerEvent.d.ts | 191 ----- .../npcs/api/event/IProjectileEvent.d.ts | 21 - .../noppes/npcs/api/event/IQuestEvent.d.ts | 23 - .../noppes/npcs/api/event/IRecipeEvent.d.ts | 33 - .../api/noppes/npcs/api/gui/IButton.d.ts | 25 - .../api/noppes/npcs/api/gui/ICustomGui.d.ts | 47 -- .../npcs/api/gui/ICustomGuiComponent.d.ts | 27 - .../api/noppes/npcs/api/gui/IItemSlot.d.ts | 14 - .../api/noppes/npcs/api/gui/ILabel.d.ts | 19 - .../api/noppes/npcs/api/gui/ILine.d.ts | 20 - .../api/noppes/npcs/api/gui/IScroll.d.ts | 19 - .../api/noppes/npcs/api/gui/ITextField.d.ts | 15 - .../noppes/npcs/api/gui/ITexturedRect.d.ts | 20 - .../npcs/api/handler/IActionManager.d.ts | 74 -- .../npcs/api/handler/IAnimationHandler.d.ts | 17 - .../npcs/api/handler/IAttributeHandler.d.ts | 13 - .../npcs/api/handler/ICloneHandler.d.ts | 19 - .../api/handler/ICustomEffectHandler.d.ts | 32 - .../npcs/api/handler/IDialogHandler.d.ts | 12 - .../npcs/api/handler/IFactionHandler.d.ts | 14 - .../npcs/api/handler/IMagicHandler.d.ts | 14 - .../api/handler/INaturalSpawnsHandler.d.ts | 16 - .../npcs/api/handler/IOverlayHandler.d.ts | 16 - .../npcs/api/handler/IPartyHandler.d.ts | 12 - .../npcs/api/handler/IPlayerBankData.d.ts | 8 - .../noppes/npcs/api/handler/IPlayerData.d.ts | 22 - .../npcs/api/handler/IPlayerDialogData.d.ts | 13 - .../npcs/api/handler/IPlayerFactionData.d.ts | 13 - .../api/handler/IPlayerItemGiverData.d.ts | 13 - .../npcs/api/handler/IPlayerMailData.d.ts | 18 - .../npcs/api/handler/IPlayerQuestData.d.ts | 21 - .../api/handler/IPlayerTransportData.d.ts | 16 - .../npcs/api/handler/IProfileHandler.d.ts | 16 - .../npcs/api/handler/IQuestHandler.d.ts | 12 - .../npcs/api/handler/IRecipeHandler.d.ts | 18 - .../noppes/npcs/api/handler/ITagHandler.d.ts | 14 - .../npcs/api/handler/ITransportHandler.d.ts | 14 - .../noppes/npcs/api/handler/data/IAction.d.ts | 80 -- .../npcs/api/handler/data/IActionChain.d.ts | 16 - .../api/handler/data/IActionListener.d.ts | 8 - .../npcs/api/handler/data/IActionQueue.d.ts | 46 -- .../npcs/api/handler/data/IAnimation.d.ts | 36 - .../npcs/api/handler/data/IAnimationData.d.ts | 19 - .../npcs/api/handler/data/IAnvilRecipe.d.ts | 16 - .../handler/data/IAttributeDefinition.d.ts | 14 - .../npcs/api/handler/data/IAvailability.d.ts | 23 - .../api/handler/data/ICustomAttribute.d.ts | 12 - .../npcs/api/handler/data/ICustomEffect.d.ts | 32 - .../noppes/npcs/api/handler/data/IDialog.d.ts | 77 -- .../api/handler/data/IDialogCategory.d.ts | 14 - .../npcs/api/handler/data/IDialogImage.d.ts | 36 - .../npcs/api/handler/data/IDialogOption.d.ts | 13 - .../npcs/api/handler/data/IFaction.d.ts | 34 - .../noppes/npcs/api/handler/data/IFrame.d.ts | 23 - .../npcs/api/handler/data/IFramePart.d.ts | 24 - .../noppes/npcs/api/handler/data/ILine.d.ts | 17 - .../noppes/npcs/api/handler/data/ILines.d.ts | 18 - .../npcs/api/handler/data/ILinkedItem.d.ts | 36 - .../noppes/npcs/api/handler/data/IMagic.d.ts | 21 - .../npcs/api/handler/data/IMagicCycle.d.ts | 17 - .../npcs/api/handler/data/IMagicData.d.ts | 17 - .../npcs/api/handler/data/INaturalSpawn.d.ts | 31 - .../noppes/npcs/api/handler/data/IParty.d.ts | 29 - .../npcs/api/handler/data/IPartyOptions.d.ts | 28 - .../api/handler/data/IPlayerAttributes.d.ts | 15 - .../npcs/api/handler/data/IPlayerEffect.d.ts | 20 - .../npcs/api/handler/data/IPlayerMail.d.ts | 23 - .../npcs/api/handler/data/IProfile.d.ts | 14 - .../api/handler/data/IProfileOptions.d.ts | 16 - .../noppes/npcs/api/handler/data/IQuest.d.ts | 36 - .../npcs/api/handler/data/IQuestCategory.d.ts | 14 - .../npcs/api/handler/data/IQuestDialog.d.ts | 8 - .../api/handler/data/IQuestInterface.d.ts | 8 - .../npcs/api/handler/data/IQuestItem.d.ts | 16 - .../npcs/api/handler/data/IQuestKill.d.ts | 12 - .../npcs/api/handler/data/IQuestLocation.d.ts | 16 - .../api/handler/data/IQuestObjective.d.ts | 17 - .../noppes/npcs/api/handler/data/IRecipe.d.ts | 23 - .../noppes/npcs/api/handler/data/ISlot.d.ts | 21 - .../noppes/npcs/api/handler/data/ISound.d.ts | 25 - .../noppes/npcs/api/handler/data/ITag.d.ts | 19 - .../api/handler/data/ITransportCategory.d.ts | 16 - .../api/handler/data/ITransportLocation.d.ts | 23 - .../data/actions/IConditionalAction.d.ts | 18 - .../api/noppes/npcs/api/item/IItemArmor.d.ts | 12 - .../api/noppes/npcs/api/item/IItemBlock.d.ts | 11 - .../api/noppes/npcs/api/item/IItemBook.d.ts | 16 - .../api/noppes/npcs/api/item/IItemCustom.d.ts | 21 - .../npcs/api/item/IItemCustomizable.d.ts | 44 -- .../api/noppes/npcs/api/item/IItemLinked.d.ts | 11 - .../api/noppes/npcs/api/item/IItemStack.d.ts | 63 -- .../api/noppes/npcs/api/jobs/IJob.d.ts | 12 - .../api/noppes/npcs/api/jobs/IJobBard.d.ts | 22 - .../npcs/api/jobs/IJobConversation.d.ts | 8 - .../noppes/npcs/api/jobs/IJobFollower.d.ts | 14 - .../api/noppes/npcs/api/jobs/IJobGuard.d.ts | 16 - .../api/noppes/npcs/api/jobs/IJobHealer.d.ts | 15 - .../noppes/npcs/api/jobs/IJobItemGiver.d.ts | 23 - .../api/noppes/npcs/api/jobs/IJobSpawner.d.ts | 16 - .../npcs/api/overlay/ICustomOverlay.d.ts | 26 - .../api/overlay/ICustomOverlayComponent.d.ts | 25 - .../npcs/api/overlay/IOverlayLabel.d.ts | 19 - .../noppes/npcs/api/overlay/IOverlayLine.d.ts | 20 - .../api/overlay/IOverlayTexturedRect.d.ts | 20 - .../api/noppes/npcs/api/roles/IRole.d.ts | 12 - .../api/noppes/npcs/api/roles/IRoleBank.d.ts | 8 - .../noppes/npcs/api/roles/IRoleFollower.d.ts | 27 - .../noppes/npcs/api/roles/IRoleMailman.d.ts | 8 - .../noppes/npcs/api/roles/IRoleTrader.d.ts | 28 - .../npcs/api/roles/IRoleTransporter.d.ts | 16 - .../npcs/api/scoreboard/IScoreboard.d.ts | 29 - .../api/scoreboard/IScoreboardObjective.d.ts | 15 - .../npcs/api/scoreboard/IScoreboardTeam.d.ts | 26 - 171 files changed, 6317 deletions(-) delete mode 100644 src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/hooks.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/index.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/util/Vec3i.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/net/minecraft/util/math/IntHashMap.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/AbstractNpcAPI.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IBlock.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ICommand.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IContainer.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IDamageSource.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/INbt.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IParticle.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPixelmonPlayerData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPos.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IScreenSize.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ISkinOverlay.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITileEntity.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITimers.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/IWorld.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/IBlockScripted.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/ITextPlane.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimal.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityItem.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGuiComponent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IItemSlot.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ILabel.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ILine.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IScroll.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ITextField.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ITexturedRect.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IActionManager.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IAnimationHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IAttributeHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ICloneHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ICustomEffectHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IDialogHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IFactionHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IMagicHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/INaturalSpawnsHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IOverlayHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPartyHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerBankData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerDialogData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerFactionData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerItemGiverData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerMailData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerQuestData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IPlayerTransportData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IProfileHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IQuestHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/IRecipeHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ITagHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/ITransportHandler.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAction.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IActionChain.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IActionListener.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IActionQueue.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAnimation.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAnimationData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAnvilRecipe.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAttributeDefinition.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IAvailability.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ICustomAttribute.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ICustomEffect.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialog.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialogCategory.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialogImage.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IDialogOption.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IFaction.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IFrame.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IFramePart.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ILine.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ILines.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ILinkedItem.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IMagic.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IMagicCycle.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IMagicData.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/INaturalSpawn.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IParty.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPartyOptions.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPlayerAttributes.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPlayerEffect.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IPlayerMail.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IProfile.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IProfileOptions.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuest.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestCategory.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestDialog.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestInterface.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestItem.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestKill.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestLocation.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IQuestObjective.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/IRecipe.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ISlot.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ISound.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ITag.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ITransportCategory.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/ITransportLocation.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/handler/data/actions/IConditionalAction.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemArmor.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemBlock.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemBook.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemCustom.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemCustomizable.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemLinked.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/item/IItemStack.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJob.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobBard.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobConversation.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobFollower.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobGuard.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobHealer.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobItemGiver.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/jobs/IJobSpawner.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/ICustomOverlay.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/ICustomOverlayComponent.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/IOverlayLabel.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/IOverlayLine.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/overlay/IOverlayTexturedRect.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRole.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleBank.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleFollower.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleMailman.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleTrader.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/roles/IRoleTransporter.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/scoreboard/IScoreboard.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/scoreboard/IScoreboardObjective.d.ts delete mode 100644 src/main/resources/assets/customnpcs/api/noppes/npcs/api/scoreboard/IScoreboardTeam.d.ts diff --git a/src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts b/src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts deleted file mode 100644 index 30846c672..000000000 --- a/src/main/resources/assets/customnpcs/api/forge-events-raw.d.ts +++ /dev/null @@ -1,697 +0,0 @@ -/** - * Specialized Raw Forge 1.7.10 Event Definitions - * Deep package hierarchy to match Java source (Forge/MCP). - */ - -declare global { - namespace cpw.mods.fml.common.eventhandler { - /** cpw.mods.fml.common.eventhandler.Event */ - export interface Event { - /** @returns If this event can be canceled */ - isCancelable(): boolean; - /** @returns If this event has been canceled */ - isCanceled(): boolean; - /** Sets the canceled state */ - setCanceled(cancel: boolean): void; - /** @returns The current result (ALLOW, DENY, DEFAULT) */ - getResult(): 'ALLOW' | 'DENY' | 'DEFAULT'; - /** Sets the result */ - setResult(result: 'ALLOW' | 'DENY' | 'DEFAULT'): void; - /** @returns If this event has a result */ - hasResult(): boolean; - } - } - - namespace cpw.mods.fml.common.gameevent { - /** cpw.mods.fml.common.gameevent.TickEvent */ - export interface TickEvent extends cpw.mods.fml.common.eventhandler.Event { - type: 'PLAYER' | 'WORLD' | 'SERVER' | 'CLIENT' | 'RENDER'; - side: 'CLIENT' | 'SERVER'; - phase: 'START' | 'END'; - } - - export namespace TickEvent { - /** cpw.mods.fml.common.gameevent.TickEvent.PlayerTickEvent */ - export interface PlayerTickEvent extends cpw.mods.fml.common.gameevent.TickEvent { - player: net.minecraft.entity.player.EntityPlayer; - } - /** cpw.mods.fml.common.gameevent.TickEvent.WorldTickEvent */ - export interface WorldTickEvent extends cpw.mods.fml.common.gameevent.TickEvent { - world: net.minecraft.world.World; - } - /** cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent */ - export type ServerTickEvent = cpw.mods.fml.common.gameevent.TickEvent - /** cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent */ - export type ClientTickEvent = cpw.mods.fml.common.gameevent.TickEvent - /** cpw.mods.fml.common.gameevent.TickEvent.RenderTickEvent */ - export interface RenderTickEvent extends cpw.mods.fml.common.gameevent.TickEvent { - renderTickTime: number; - } - } - - /** cpw.mods.fml.common.gameevent.InputEvent */ - export type InputEvent = cpw.mods.fml.common.eventhandler.Event - export namespace InputEvent { - /** cpw.mods.fml.common.gameevent.InputEvent.KeyInputEvent */ - export type KeyInputEvent = InputEvent - /** cpw.mods.fml.common.gameevent.InputEvent.MouseInputEvent */ - export type MouseInputEvent = InputEvent - } - - /** cpw.mods.fml.common.gameevent.PlayerEvent */ - export interface PlayerEvent extends cpw.mods.fml.common.eventhandler.Event { - player: net.minecraft.entity.player.EntityPlayer; - } - export namespace PlayerEvent { - /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent */ - export type PlayerLoggedInEvent = cpw.mods.fml.common.gameevent.PlayerEvent - /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent */ - export type PlayerLoggedOutEvent = cpw.mods.fml.common.gameevent.PlayerEvent - /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerRespawnEvent */ - export type PlayerRespawnEvent = cpw.mods.fml.common.gameevent.PlayerEvent - /** cpw.mods.fml.common.gameevent.PlayerEvent.PlayerChangedDimensionEvent */ - export interface PlayerChangedDimensionEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { - fromDim: number; - toDim: number; - } - /** cpw.mods.fml.common.gameevent.PlayerEvent.ItemPickupEvent */ - export interface ItemPickupEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { - pickedUp: net.minecraft.entity.EntityItem; - } - /** cpw.mods.fml.common.gameevent.PlayerEvent.ItemCraftedEvent */ - export interface ItemCraftedEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { - craftMatrix: any; - crafting: net.minecraft.item.ItemStack; - } - /** cpw.mods.fml.common.gameevent.PlayerEvent.ItemSmeltedEvent */ - export interface ItemSmeltedEvent extends cpw.mods.fml.common.gameevent.PlayerEvent { - smelting: net.minecraft.item.ItemStack; - } - } - } - - namespace net.minecraftforge.event { - /** net.minecraftforge.event.AnvilUpdateEvent */ - export interface AnvilUpdateEvent extends cpw.mods.fml.common.eventhandler.Event { - left: net.minecraft.item.ItemStack; - right: net.minecraft.item.ItemStack; - name: string; - output: net.minecraft.item.ItemStack; - cost: number; - materialCost: number; - } - - /** net.minecraftforge.event.CommandEvent */ - export interface CommandEvent extends cpw.mods.fml.common.eventhandler.Event { - command: any; - sender: any; - parameters: string[]; - exception: any; - } - - /** net.minecraftforge.event.ServerChatEvent */ - export interface ServerChatEvent extends cpw.mods.fml.common.eventhandler.Event { - message: string; - username: string; - player: net.minecraft.entity.player.EntityPlayer; - component: any; - } - - /** net.minecraftforge.event.FuelBurnTimeEvent */ - export interface FuelBurnTimeEvent extends cpw.mods.fml.common.eventhandler.Event { - fuel: net.minecraft.item.ItemStack; - burnTime: number; - } - - export namespace brewing { - /** net.minecraftforge.event.brewing.PotionBrewEvent */ - export interface PotionBrewEvent extends cpw.mods.fml.common.eventhandler.Event { - getItem(index: number): net.minecraft.item.ItemStack; - setItem(index: number, stack: net.minecraft.item.ItemStack): void; - getLength(): number; - } - export namespace PotionBrewEvent { - export type Pre = PotionBrewEvent - export type Post = PotionBrewEvent - } - /** net.minecraftforge.event.brewing.PotionBrewedEvent */ - export interface PotionBrewedEvent extends cpw.mods.fml.common.eventhandler.Event { - brewingStacks: net.minecraft.item.ItemStack[]; - } - } - - export namespace entity { - /** net.minecraftforge.event.entity.EntityEvent */ - export interface EntityEvent extends cpw.mods.fml.common.eventhandler.Event { - entity: net.minecraft.entity.Entity; - } - export namespace EntityEvent { - /** net.minecraftforge.event.entity.EntityEvent.EntityConstructing */ - export type EntityConstructing = net.minecraftforge.event.entity.EntityEvent - /** net.minecraftforge.event.entity.EntityEvent.CanUpdate */ - export interface CanUpdate extends net.minecraftforge.event.entity.EntityEvent { - canUpdate: boolean; - } - /** net.minecraftforge.event.entity.EntityEvent.EnteringChunk */ - export interface EnteringChunk extends net.minecraftforge.event.entity.EntityEvent { - newChunkX: number; - newChunkZ: number; - oldChunkX: number; - oldChunkZ: number; - } - } - - /** net.minecraftforge.event.entity.EntityJoinWorldEvent */ - export interface EntityJoinWorldEvent extends net.minecraftforge.event.entity.EntityEvent { - world: net.minecraft.world.World; - } - - /** net.minecraftforge.event.entity.EntityStruckByLightningEvent */ - export interface EntityStruckByLightningEvent extends net.minecraftforge.event.entity.EntityEvent { - lightning: net.minecraft.entity.Entity; - } - - /** net.minecraftforge.event.entity.PlaySoundAtEntityEvent */ - export interface PlaySoundAtEntityEvent extends net.minecraftforge.event.entity.EntityEvent { - name: string; - volume: number; - pitch: number; - } - - export namespace item { - /** net.minecraftforge.event.entity.item.ItemEvent */ - export interface ItemEvent extends net.minecraftforge.event.entity.EntityEvent { - entityItem: net.minecraft.entity.EntityItem; - } - /** net.minecraftforge.event.entity.item.ItemExpireEvent */ - export interface ItemExpireEvent extends net.minecraftforge.event.entity.item.ItemEvent { - extraLife: number; - } - /** net.minecraftforge.event.entity.item.ItemTossEvent */ - export interface ItemTossEvent extends net.minecraftforge.event.entity.item.ItemEvent { - player: net.minecraft.entity.player.EntityPlayer; - } - } - - export namespace living { - /** net.minecraftforge.event.entity.living.LivingEvent */ - export interface LivingEvent extends net.minecraftforge.event.entity.EntityEvent { - entityLiving: net.minecraft.entity.EntityLivingBase; - } - export namespace LivingEvent { - /** net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent */ - export type LivingUpdateEvent = net.minecraftforge.event.entity.living.LivingEvent - /** net.minecraftforge.event.entity.living.LivingEvent.LivingJumpEvent */ - export type LivingJumpEvent = net.minecraftforge.event.entity.living.LivingEvent - } - - /** net.minecraftforge.event.entity.living.LivingAttackEvent */ - export interface LivingAttackEvent extends net.minecraftforge.event.entity.living.LivingEvent { - source: net.minecraft.damage.DamageSource; - ammount: number; - } - - /** net.minecraftforge.event.entity.living.LivingDeathEvent */ - export interface LivingDeathEvent extends net.minecraftforge.event.entity.living.LivingEvent { - source: net.minecraft.damage.DamageSource; - } - - /** net.minecraftforge.event.entity.living.LivingDropsEvent */ - export interface LivingDropsEvent extends net.minecraftforge.event.entity.living.LivingEvent { - source: net.minecraft.damage.DamageSource; - drops: any[]; - lootingLevel: number; - recentlyHit: boolean; - specialDropValue: number; - } - - /** net.minecraftforge.event.entity.living.LivingFallEvent */ - export interface LivingFallEvent extends net.minecraftforge.event.entity.living.LivingEvent { - distance: number; - } - - /** net.minecraftforge.event.entity.living.LivingHealEvent */ - export interface LivingHealEvent extends net.minecraftforge.event.entity.living.LivingEvent { - amount: number; - } - - /** net.minecraftforge.event.entity.living.LivingHurtEvent */ - export interface LivingHurtEvent extends net.minecraftforge.event.entity.living.LivingEvent { - source: net.minecraft.damage.DamageSource; - ammount: number; - } - - /** net.minecraftforge.event.entity.living.LivingPackSizeEvent */ - export interface LivingPackSizeEvent extends net.minecraftforge.event.entity.living.LivingEvent { - maxPackSize: number; - } - - /** net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent */ - export interface LivingSetAttackTargetEvent extends net.minecraftforge.event.entity.living.LivingEvent { - target: net.minecraft.entity.EntityLivingBase; - } - - /** net.minecraftforge.event.entity.living.EnderTeleportEvent */ - export interface EnderTeleportEvent extends net.minecraftforge.event.entity.living.LivingEvent { - targetX: number; - targetY: number; - targetZ: number; - attackDamage: number; - } - - export namespace LivingSpawnEvent { - /** net.minecraftforge.event.entity.living.LivingSpawnEvent */ - export interface LivingSpawnEvent extends net.minecraftforge.event.entity.living.LivingEvent { - world: net.minecraft.world.World; - x: number; - y: number; - z: number; - } - /** net.minecraftforge.event.entity.living.LivingSpawnEvent.CheckSpawn */ - export type CheckSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.LivingSpawnEvent - /** net.minecraftforge.event.entity.living.LivingSpawnEvent.SpecialSpawn */ - export type SpecialSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.LivingSpawnEvent - /** net.minecraftforge.event.entity.living.LivingSpawnEvent.AllowDespawn */ - export type AllowDespawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.LivingSpawnEvent - } - - export namespace ZombieEvent { - /** net.minecraftforge.event.entity.living.ZombieEvent.SummonAidEvent */ - export interface SummonAidEvent extends net.minecraftforge.event.entity.EntityEvent { - customSummonedAid: net.minecraft.entity.EntityZombie; - world: net.minecraft.world.World; - x: number; - y: number; - z: number; - attacker: net.minecraft.entity.EntityLivingBase; - summonChance: number; - } - } - } - - export namespace player { - /** net.minecraftforge.event.entity.player.PlayerEvent */ - export interface PlayerEvent extends net.minecraftforge.event.entity.living.LivingEvent { - /** WARNING: Forge 1.7.10 uses 'entityPlayer' as the field name in this class! */ - entityPlayer: net.minecraft.entity.player.EntityPlayer; - } - export namespace PlayerEvent { - /** net.minecraftforge.event.entity.player.PlayerEvent.HarvestCheck */ - export interface HarvestCheck extends net.minecraftforge.event.entity.player.PlayerEvent { - block: net.minecraft.block.Block; - success: boolean; - } - /** net.minecraftforge.event.entity.player.PlayerEvent.BreakSpeed */ - export interface BreakSpeed extends net.minecraftforge.event.entity.player.PlayerEvent { - block: net.minecraft.block.Block; - metadata: number; - originalSpeed: number; - newSpeed: number; - x: number; - y: number; - z: number; - } - /** net.minecraftforge.event.entity.player.PlayerEvent.NameFormat */ - export interface NameFormat extends net.minecraftforge.event.entity.player.PlayerEvent { - username: string; - displayname: string; - } - /** net.minecraftforge.event.entity.player.PlayerEvent.Clone */ - export interface Clone extends net.minecraftforge.event.entity.player.PlayerEvent { - original: net.minecraft.entity.player.EntityPlayer; - wasDeath: boolean; - } - /** net.minecraftforge.event.entity.player.PlayerEvent.StartTracking */ - export interface StartTracking extends net.minecraftforge.event.entity.player.PlayerEvent { - target: net.minecraft.entity.Entity; - } - /** net.minecraftforge.event.entity.player.PlayerEvent.StopTracking */ - export interface StopTracking extends net.minecraftforge.event.entity.player.PlayerEvent { - target: net.minecraft.entity.Entity; - } - /** net.minecraftforge.event.entity.player.PlayerEvent.LoadFromFile */ - export interface LoadFromFile extends net.minecraftforge.event.entity.player.PlayerEvent { - playerDirectory: any; - playerUUID: string; - getPlayerFile(suffix: string): any; - } - /** net.minecraftforge.event.entity.player.PlayerEvent.SaveToFile */ - export interface SaveToFile extends net.minecraftforge.event.entity.player.PlayerEvent { - playerDirectory: any; - playerUUID: string; - getPlayerFile(suffix: string): any; - } - } - - /** net.minecraftforge.event.entity.player.AchievementEvent */ - export interface AchievementEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - achievement: any; - } - - /** net.minecraftforge.event.entity.player.AnvilRepairEvent */ - export interface AnvilRepairEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - left: net.minecraft.item.ItemStack; - right: net.minecraft.item.ItemStack; - output: net.minecraft.item.ItemStack; - } - - /** net.minecraftforge.event.entity.player.ArrowLooseEvent */ - export interface ArrowLooseEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - bow: net.minecraft.item.ItemStack; - charge: number; - } - - /** net.minecraftforge.event.entity.player.ArrowNockEvent */ - export interface ArrowNockEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - result: net.minecraft.item.ItemStack; - } - - /** net.minecraftforge.event.entity.player.AttackEntityEvent */ - export interface AttackEntityEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - target: net.minecraft.entity.Entity; - } - - /** net.minecraftforge.event.entity.player.BonemealEvent */ - export interface BonemealEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - world: net.minecraft.world.World; - block: net.minecraft.block.Block; - x: number; - y: number; - z: number; - } - - /** net.minecraftforge.event.entity.player.EntityInteractEvent */ - export interface EntityInteractEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - target: net.minecraft.entity.Entity; - } - - /** net.minecraftforge.event.entity.player.EntityItemPickupEvent */ - export interface EntityItemPickupEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - item: net.minecraft.entity.EntityItem; - } - - /** net.minecraftforge.event.entity.player.FillBucketEvent */ - export interface FillBucketEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - current: net.minecraft.item.ItemStack; - result: net.minecraft.item.ItemStack; - world: net.minecraft.world.World; - target: net.minecraft.util.MovingObjectPosition; - } - - /** net.minecraftforge.event.entity.player.ItemTooltipEvent */ - export interface ItemTooltipEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - showAdvancedItemTooltips: boolean; - itemStack: net.minecraft.item.ItemStack; - toolTip: string[]; - } - - /** net.minecraftforge.event.entity.player.PlayerDestroyItemEvent */ - export interface PlayerDestroyItemEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - original: net.minecraft.item.ItemStack; - } - - /** net.minecraftforge.event.entity.player.PlayerDropsEvent */ - export interface PlayerDropsEvent extends net.minecraftforge.event.entity.living.LivingDropsEvent { - entityPlayer: net.minecraft.entity.player.EntityPlayer; - } - - /** net.minecraftforge.event.entity.player.PlayerFlyableFallEvent */ - export interface PlayerFlyableFallEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - distance: number; - } - - /** net.minecraftforge.event.entity.player.PlayerInteractEvent */ - export interface PlayerInteractEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - action: 'RIGHT_CLICK_AIR' | 'RIGHT_CLICK_BLOCK' | 'LEFT_CLICK_BLOCK'; - x: number; - y: number; - z: number; - face: number; - world: net.minecraft.world.World; - useBlock: 'ALLOW' | 'DENY' | 'DEFAULT'; - useItem: 'ALLOW' | 'DENY' | 'DEFAULT'; - } - - /** net.minecraftforge.event.entity.player.PlayerOpenContainerEvent */ - export interface PlayerOpenContainerEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - canInteractWith: boolean; - } - - /** net.minecraftforge.event.entity.player.PlayerPickupXpEvent */ - export interface PlayerPickupXpEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - orb: any; - } - - /** net.minecraftforge.event.entity.player.PlayerSleepInBedEvent */ - export interface PlayerSleepInBedEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - x: number; - y: number; - z: number; - result: 'OK' | 'NOT_POSSIBLE_HERE' | 'NOT_POSSIBLE_NOW' | 'TOO_FAR_AWAY' | 'OTHER_PROBLEM' | 'NOT_SAFE'; - } - - /** net.minecraftforge.event.entity.player.PlayerUseItemEvent */ - export interface PlayerUseItemEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - item: net.minecraft.item.ItemStack; - duration: number; - } - export namespace PlayerUseItemEvent { - export type Start = net.minecraftforge.event.entity.player.PlayerUseItemEvent - export type Tick = net.minecraftforge.event.entity.player.PlayerUseItemEvent - export type Stop = net.minecraftforge.event.entity.player.PlayerUseItemEvent - export type Finish = net.minecraftforge.event.entity.player.PlayerUseItemEvent - } - - /** net.minecraftforge.event.entity.player.PlayerWakeUpEvent */ - export interface PlayerWakeUpEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - wakeImmediately: boolean; - updateWorld: boolean; - setSpawn: boolean; - } - - /** net.minecraftforge.event.entity.player.UseHoeEvent */ - export interface UseHoeEvent extends net.minecraftforge.event.entity.player.PlayerEvent { - current: net.minecraft.item.ItemStack; - world: net.minecraft.world.World; - x: number; - y: number; - z: number; - } - } - } - - export namespace world { - /** net.minecraftforge.event.world.WorldEvent */ - export interface WorldEvent extends cpw.mods.fml.common.eventhandler.Event { - world: net.minecraft.world.World; - } - export namespace WorldEvent { - export type Load = net.minecraftforge.event.world.WorldEvent - export type Unload = net.minecraftforge.event.world.WorldEvent - export type Save = net.minecraftforge.event.world.WorldEvent - export interface PotentialSpawns extends net.minecraftforge.event.world.WorldEvent { - type: any; - x: number; - y: number; - z: number; - list: any[]; - } - export interface CreateSpawnPosition extends net.minecraftforge.event.world.WorldEvent { - settings: any; - } - } - - export namespace BlockEvent { - /** net.minecraftforge.event.world.BlockEvent */ - export interface BlockEvent extends cpw.mods.fml.common.eventhandler.Event { - x: number; - y: number; - z: number; - world: net.minecraft.world.World; - block: net.minecraft.block.Block; - blockMetadata: number; - } - /** net.minecraftforge.event.world.BlockEvent.BreakEvent */ - export interface BreakEvent extends net.minecraftforge.event.world.BlockEvent.BlockEvent { - /** @returns The player who broke the block */ - getPlayer(): net.minecraft.entity.player.EntityPlayer; - /** @returns The experience to drop */ - getExpToDrop(): number; - /** Sets the experience to drop */ - setExpToDrop(exp: number): void; - } - /** net.minecraftforge.event.world.BlockEvent.HarvestDropsEvent */ - export interface HarvestDropsEvent extends net.minecraftforge.event.world.BlockEvent.BlockEvent { - fortuneLevel: number; - drops: net.minecraft.item.ItemStack[]; - isSilkTouching: boolean; - dropChance: number; - /** May be null for non-player harvesting */ - harvester: net.minecraft.entity.player.EntityPlayer; - } - /** net.minecraftforge.event.world.BlockEvent.PlaceEvent */ - export interface PlaceEvent extends net.minecraftforge.event.world.BlockEvent.BlockEvent { - player: net.minecraft.entity.player.EntityPlayer; - itemInHand: net.minecraft.item.ItemStack; - placedBlock: net.minecraft.block.Block; - placedAgainst: net.minecraft.block.Block; - /** Note: BlockSnapshot is a complex structure, simplifying here */ - blockSnapshot: any; - } - /** net.minecraftforge.event.world.BlockEvent.MultiPlaceEvent */ - export interface MultiPlaceEvent extends net.minecraftforge.event.world.BlockEvent.PlaceEvent { - /** @returns A list of replaced block snapshots */ - getReplacedBlockSnapshots(): any[]; - } - } - - export namespace explosion { - /** net.minecraftforge.event.world.ExplosionEvent */ - export interface ExplosionEvent extends cpw.mods.fml.common.eventhandler.Event { - world: net.minecraft.world.World; - explosion: any; - } - export namespace ExplosionEvent { - export type Start = net.minecraftforge.event.world.explosion.ExplosionEvent - export interface Detonate extends net.minecraftforge.event.world.explosion.ExplosionEvent { - affectedEntities: net.minecraft.entity.Entity[]; - } - } - } - - export namespace note { - /** net.minecraftforge.event.world.NoteBlockEvent */ - export type NoteBlockEvent = net.minecraftforge.event.world.BlockEvent.BlockEvent - export namespace NoteBlockEvent { - export interface Play extends net.minecraftforge.event.world.note.NoteBlockEvent { - instrument: any; - note: number; - } - export interface Change extends net.minecraftforge.event.world.note.NoteBlockEvent { - oldInstrument: any; - oldNote: number; - newInstrument: any; - newNote: number; - } - } - } - } - } - - // ============================================================================ - // GLOBAL ALIASES - To match CustomNPC+ pattern (e.g., IForgeEvent.AnvilUpdateEvent) - // Points to the new deep structure to ensure correct tooltips. - // ============================================================================ - namespace IForgeEvent { - export type InitEvent = cpw.mods.fml.common.eventhandler.Event; - - export type AnvilUpdateEvent = net.minecraftforge.event.AnvilUpdateEvent; - export type CommandEvent = net.minecraftforge.event.CommandEvent; - export type ServerChatEvent = net.minecraftforge.event.ServerChatEvent; - export type FuelBurnTimeEvent = net.minecraftforge.event.FuelBurnTimeEvent; - - export type PotionBrewEvent = net.minecraftforge.event.brewing.PotionBrewEvent; - export type PotionBrewedEvent = net.minecraftforge.event.brewing.PotionBrewedEvent; - - export type EntityConstructing = net.minecraftforge.event.entity.EntityEvent.EntityConstructing; - export type CanUpdate = net.minecraftforge.event.entity.EntityEvent.CanUpdate; - export type EnteringChunk = net.minecraftforge.event.entity.EntityEvent.EnteringChunk; - export type EntityJoinWorldEvent = net.minecraftforge.event.entity.EntityJoinWorldEvent; - export type EntityStruckByLightningEvent = net.minecraftforge.event.entity.EntityStruckByLightningEvent; - export type PlaySoundAtEntityEvent = net.minecraftforge.event.entity.PlaySoundAtEntityEvent; - - export type ItemEvent = net.minecraftforge.event.entity.item.ItemEvent; - export type ItemExpireEvent = net.minecraftforge.event.entity.item.ItemExpireEvent; - export type ItemTossEvent = net.minecraftforge.event.entity.item.ItemTossEvent; - - export type LivingUpdateEvent = net.minecraftforge.event.entity.living.LivingEvent.LivingUpdateEvent; - export type LivingJumpEvent = net.minecraftforge.event.entity.living.LivingEvent.LivingJumpEvent; - export type LivingAttackEvent = net.minecraftforge.event.entity.living.LivingAttackEvent; - export type LivingDeathEvent = net.minecraftforge.event.entity.living.LivingDeathEvent; - export type LivingDropsEvent = net.minecraftforge.event.entity.living.LivingDropsEvent; - export type LivingFallEvent = net.minecraftforge.event.entity.living.LivingFallEvent; - export type LivingHealEvent = net.minecraftforge.event.entity.living.LivingHealEvent; - export type LivingHurtEvent = net.minecraftforge.event.entity.living.LivingHurtEvent; - export type LivingPackSizeEvent = net.minecraftforge.event.entity.living.LivingPackSizeEvent; - export type LivingSetAttackTargetEvent = net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent; - export type EnderTeleportEvent = net.minecraftforge.event.entity.living.EnderTeleportEvent; - - export type CheckSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.CheckSpawn; - export type SpecialSpawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.SpecialSpawn; - export type AllowDespawn = net.minecraftforge.event.entity.living.LivingSpawnEvent.AllowDespawn; - export type SummonAidEvent = net.minecraftforge.event.entity.living.ZombieEvent.SummonAidEvent; - - export type HarvestCheck = net.minecraftforge.event.entity.player.PlayerEvent.HarvestCheck; - export type BreakSpeed = net.minecraftforge.event.entity.player.PlayerEvent.BreakSpeed; - export type NameFormat = net.minecraftforge.event.entity.player.PlayerEvent.NameFormat; - export type Clone = net.minecraftforge.event.entity.player.PlayerEvent.Clone; - export type StartTracking = net.minecraftforge.event.entity.player.PlayerEvent.StartTracking; - export type StopTracking = net.minecraftforge.event.entity.player.PlayerEvent.StopTracking; - export type LoadFromFile = net.minecraftforge.event.entity.player.PlayerEvent.LoadFromFile; - export type SaveToFile = net.minecraftforge.event.entity.player.PlayerEvent.SaveToFile; - - export type AchievementEvent = net.minecraftforge.event.entity.player.AchievementEvent; - export type AnvilRepairEvent = net.minecraftforge.event.entity.player.AnvilRepairEvent; - export type ArrowLooseEvent = net.minecraftforge.event.entity.player.ArrowLooseEvent; - export type ArrowNockEvent = net.minecraftforge.event.entity.player.ArrowNockEvent; - export type AttackEntityEvent = net.minecraftforge.event.entity.player.AttackEntityEvent; - export type BonemealEvent = net.minecraftforge.event.entity.player.BonemealEvent; - export type EntityInteractEvent = net.minecraftforge.event.entity.player.EntityInteractEvent; - export type EntityItemPickupEvent = net.minecraftforge.event.entity.player.EntityItemPickupEvent; - export type FillBucketEvent = net.minecraftforge.event.entity.player.FillBucketEvent; - export type ItemTooltipEvent = net.minecraftforge.event.entity.player.ItemTooltipEvent; - export type PlayerDestroyItemEvent = net.minecraftforge.event.entity.player.PlayerDestroyItemEvent; - export type PlayerDropsEvent = net.minecraftforge.event.entity.player.PlayerDropsEvent; - export type PlayerFlyableFallEvent = net.minecraftforge.event.entity.player.PlayerFlyableFallEvent; - export type PlayerInteractEvent = net.minecraftforge.event.entity.player.PlayerInteractEvent; - export type PlayerOpenContainerEvent = net.minecraftforge.event.entity.player.PlayerOpenContainerEvent; - export type PlayerPickupXpEvent = net.minecraftforge.event.entity.player.PlayerPickupXpEvent; - export type PlayerSleepInBedEvent = net.minecraftforge.event.entity.player.PlayerSleepInBedEvent; - export type UseHoeEvent = net.minecraftforge.event.entity.player.UseHoeEvent; - - export type PlayerUseItemStart = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Start; - export type PlayerUseItemTick = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Tick; - export type PlayerUseItemStop = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Stop; - export type PlayerUseItemFinish = net.minecraftforge.event.entity.player.PlayerUseItemEvent.Finish; - export type PlayerWakeUpEvent = net.minecraftforge.event.entity.player.PlayerWakeUpEvent; - - export type WorldLoad = net.minecraftforge.event.world.WorldEvent.Load; - export type WorldUnload = net.minecraftforge.event.world.WorldEvent.Unload; - export type WorldSave = net.minecraftforge.event.world.WorldEvent.Save; - export type PotentialSpawns = net.minecraftforge.event.world.WorldEvent.PotentialSpawns; - export type CreateSpawnPosition = net.minecraftforge.event.world.WorldEvent.CreateSpawnPosition; - - export type BreakEvent = net.minecraftforge.event.world.BlockEvent.BreakEvent; - export type HarvestDropsEvent = net.minecraftforge.event.world.BlockEvent.HarvestDropsEvent; - export type PlaceEvent = net.minecraftforge.event.world.BlockEvent.PlaceEvent; - export type MultiPlaceEvent = net.minecraftforge.event.world.BlockEvent.MultiPlaceEvent; - export type BlockEvent = net.minecraftforge.event.world.BlockEvent.BlockEvent; - - export type ExplosionStart = net.minecraftforge.event.world.explosion.ExplosionEvent.Start; - export type ExplosionDetonate = net.minecraftforge.event.world.explosion.ExplosionEvent.Detonate; - - export type NotePlay = net.minecraftforge.event.world.note.NoteBlockEvent.Play; - export type NoteChange = net.minecraftforge.event.world.note.NoteBlockEvent.Change; - - // FML Events - export type PlayerTickEvent = cpw.mods.fml.common.gameevent.TickEvent.PlayerTickEvent; - export type WorldTickEvent = cpw.mods.fml.common.gameevent.TickEvent.WorldTickEvent; - export type ServerTickEvent = cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent; - export type ClientTickEvent = cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent; - export type RenderTickEvent = cpw.mods.fml.common.gameevent.TickEvent.RenderTickEvent; - export type TickEvent = cpw.mods.fml.common.gameevent.TickEvent; - - export type KeyInputEvent = cpw.mods.fml.common.gameevent.InputEvent.KeyInputEvent; - export type MouseInputEvent = cpw.mods.fml.common.gameevent.InputEvent.MouseInputEvent; - - export type PlayerLoggedInEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent; - export type PlayerLoggedOutEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent; - export type PlayerRespawnEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerRespawnEvent; - export type PlayerChangedDimensionEvent = cpw.mods.fml.common.gameevent.PlayerEvent.PlayerChangedDimensionEvent; - export type PlayerItemPickupEvent = cpw.mods.fml.common.gameevent.PlayerEvent.ItemPickupEvent; - export type PlayerItemCraftedEvent = cpw.mods.fml.common.gameevent.PlayerEvent.ItemCraftedEvent; - export type PlayerItemSmeltedEvent = cpw.mods.fml.common.gameevent.PlayerEvent.ItemSmeltedEvent; - } -} - -export { }; diff --git a/src/main/resources/assets/customnpcs/api/hooks.d.ts b/src/main/resources/assets/customnpcs/api/hooks.d.ts deleted file mode 100644 index 3a5811ab8..000000000 --- a/src/main/resources/assets/customnpcs/api/hooks.d.ts +++ /dev/null @@ -1,259 +0,0 @@ -import './minecraft-raw.d.ts'; -import './forge-events-raw.d.ts'; - -/** - * CustomNPC+ Event Hook Overloads - * - * Functions are grouped by name to enable precise type inference. - * For common hooks (init, tick, damaged), parameter names (INpcEvent, IPlayerEvent, etc.) - * are used to help the LSP distinguish between multiple valid overloads. - */ - -declare global { - // ============ Global Lifecycle & Common Hooks ============ - function init(INpcEvent: INpcEvent.InitEvent): void; - function init(IPlayerEvent: IPlayerEvent.InitEvent): void; - function init(IItemEvent: IItemEvent.InitEvent): void; - function init(IBlockEvent: IBlockEvent.InitEvent): void; - function init(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; - - function tick(INpcEvent: INpcEvent.UpdateEvent): void; - function tick(IPlayerEvent: IPlayerEvent.UpdateEvent): void; - function tick(IItemEvent: IItemEvent.UpdateEvent): void; - function tick(IBlockEvent: IBlockEvent.UpdateEvent): void; - - function interact(INpcEvent: INpcEvent.InteractEvent): void; - function interact(IPlayerEvent: IPlayerEvent.InteractEvent): void; - function interact(IItemEvent: IItemEvent.InteractEvent): void; - function interact(IBlockEvent: IBlockEvent.InteractEvent): void; - - function rightClick(IPlayerEvent: IPlayerEvent.RightClickEvent): void; - function rightClick(IItemEvent: IItemEvent.RightClickEvent): void; - - function attack(IPlayerEvent: IPlayerEvent.AttackEvent): void; - function attack(IItemEvent: IItemEvent.AttackEvent): void; - - function damaged(INpcEvent: INpcEvent.DamagedEvent): void; - function damaged(IPlayerEvent: IPlayerEvent.DamagedEvent): void; - function damaged(ForgeEvent: net.minecraftforge.event.entity.living.LivingHurtEvent): void; - - function killed(INpcEvent: INpcEvent.DiedEvent): void; - function killed(IPlayerEvent: IPlayerEvent.DiedEvent): void; - - function kills(INpcEvent: INpcEvent.KilledEntityEvent): void; - function kills(IPlayerEvent: IPlayerEvent.KilledEntityEvent): void; - - function timer(INpcEvent: INpcEvent.TimerEvent): void; - function timer(IPlayerEvent: IPlayerEvent.TimerEvent): void; - function timer(IBlockEvent: IBlockEvent.TimerEvent): void; - - function dialogClose(INpcEvent: INpcEvent.DialogClosedEvent): void; - function dialogClose(IDialogEvent: IDialogEvent.DialogClosed): void; - - function rangedLaunched(INpcEvent: INpcEvent.RangedLaunchedEvent): void; - function rangedLaunched(IPlayerEvent: IPlayerEvent.RangedLaunchedEvent): void; - - function startItem(IPlayerEvent: IPlayerEvent.StartUsingItem): void; - function startItem(IItemEvent: IItemEvent.StartUsingItem): void; - - function usingItem(IPlayerEvent: IPlayerEvent.UsingItem): void; - function usingItem(IItemEvent: IItemEvent.UsingItem): void; - - function stopItem(IPlayerEvent: IPlayerEvent.StopUsingItem): void; - function stopItem(IItemEvent: IItemEvent.StopUsingItem): void; - - function finishItem(IPlayerEvent: IPlayerEvent.FinishUsingItem): void; - function finishItem(IItemEvent: IItemEvent.FinishUsingItem): void; - - // ============ Player Specific Hooks ============ - function attacked(IPlayerEvent: IPlayerEvent.AttackedEvent): void; - function damagedEntity(IPlayerEvent: IPlayerEvent.DamagedEntityEvent): void; - function drop(IPlayerEvent: IPlayerEvent.DropEvent): void; - function respawn(IPlayerEvent: IPlayerEvent.RespawnEvent): void; - function breakBlock(IPlayerEvent: IPlayerEvent.BreakEvent): void; - function chat(IPlayerEvent: IPlayerEvent.ChatEvent): void; - function login(IPlayerEvent: IPlayerEvent.LoginEvent): void; - function logout(IPlayerEvent: IPlayerEvent.LogoutEvent): void; - function keyPressed(IPlayerEvent: IPlayerEvent.KeyPressedEvent): void; - function mouseClicked(IPlayerEvent: IPlayerEvent.MouseClickedEvent): void; - function toss(IPlayerEvent: IPlayerEvent.TossEvent): void; - function pickUp(IPlayerEvent: IPlayerEvent.PickUpEvent): void; - function pickupXP(IPlayerEvent: IPlayerEvent.PickupXPEvent): void; - function rangedCharge(IPlayerEvent: IPlayerEvent.RangedChargeEvent): void; - function containerOpen(IPlayerEvent: IPlayerEvent.ContainerOpen): void; - function useHoe(IPlayerEvent: IPlayerEvent.UseHoeEvent): void; - function bonemeal(IPlayerEvent: IPlayerEvent.BonemealEvent): void; - function fillBucket(IPlayerEvent: IPlayerEvent.FillBucketEvent): void; - function jump(IPlayerEvent: IPlayerEvent.JumpEvent): void; - function fall(IPlayerEvent: IPlayerEvent.FallEvent): void; - function wakeUp(IPlayerEvent: IPlayerEvent.WakeUpEvent): void; - function sleep(IPlayerEvent: IPlayerEvent.SleepEvent): void; - function playSound(IPlayerEvent: IPlayerEvent.SoundEvent): void; - function lightning(IPlayerEvent: IPlayerEvent.LightningEvent): void; - function changedDim(IPlayerEvent: IPlayerEvent.ChangedDimension): void; - - // ============ NPC Specific Hooks ============ - function dialog(INpcEvent: INpcEvent.DialogEvent): void; - function meleeAttack(INpcEvent: INpcEvent.MeleeAttackEvent): void; - function meleeSwing(INpcEvent: INpcEvent.SwingEvent): void; - function target(INpcEvent: INpcEvent.TargetEvent): void; - function collide(INpcEvent: INpcEvent.CollideEvent): void; - function targetLost(INpcEvent: INpcEvent.TargetLostEvent): void; - - // ============ Item Specific Hooks ============ - function tossed(IItemEvent: IItemEvent.TossedEvent): void; - function pickedUp(IItemEvent: IItemEvent.PickedUpEvent): void; - function spawn(IItemEvent: IItemEvent.SpawnEvent): void; - - // ============ Projectile Specific Hooks ============ - function projectileTick(IProjectileEvent: IProjectileEvent.UpdateEvent): void; - function projectileImpact(IProjectileEvent: IProjectileEvent.ImpactEvent): void; - - // ============ Block Specific Hooks ============ - function redstone(IBlockEvent: IBlockEvent.RedstoneEvent): void; - function broken(IBlockEvent: IBlockEvent.BreakEvent): void; - function exploded(IBlockEvent: IBlockEvent.ExplodedEvent): void; - function rainFilled(IBlockEvent: IBlockEvent.RainFillEvent): void; - function neighborChanged(IBlockEvent: IBlockEvent.NeighborChangedEvent): void; - function clicked(IBlockEvent: IBlockEvent.ClickedEvent): void; - function harvested(IBlockEvent: IBlockEvent.HarvestedEvent): void; - function collide(IBlockEvent: IBlockEvent.CollidedEvent): void; - function fallenUpon(IBlockEvent: IBlockEvent.EntityFallenUponEvent): void; - - // ============ Specialized Systems (Quest, Dialog, GUI, etc.) ============ - function questStart(IQuestEvent: IQuestEvent.QuestStartEvent): void; - function questCompleted(IQuestEvent: IQuestEvent.QuestCompletedEvent): void; - function questTurnIn(IQuestEvent: IQuestEvent.QuestTurnedInEvent): void; - function factionPoints(IFactionEvent: IFactionEvent.FactionPointsEvent): void; - function dialogOpen(IDialogEvent: IDialogEvent.DialogClosed): void; - function dialogOption(IDialogEvent: IDialogEvent.DialogOption): void; - function customGuiClosed(ICustomGuiEvent: ICustomGuiEvent.CloseEvent): void; - function customGuiButton(ICustomGuiEvent: ICustomGuiEvent.ButtonEvent): void; - function customGuiSlot(ICustomGuiEvent: ICustomGuiEvent.SlotEvent): void; - function customGuiSlotClicked(ICustomGuiEvent: ICustomGuiEvent.SlotClickEvent): void; - function customGuiScroll(ICustomGuiEvent: ICustomGuiEvent.ScrollEvent): void; - - // ============ DBC Addon Hooks ============ - function dbcFormChange(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.FormChangeEvent): void; - function dbcDamaged(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.DamagedEvent): void; - function dbcCapsuleUsed(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.CapsuleUsedEvent): void; - function dbcSenzuUsed(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.SenzuUsedEvent): void; - function dbcRevived(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.DBCReviveEvent): void; - function dbcKnockout(IDBCEvent: import('./kamkeel/npcdbc/api/event/IDBCEvent').IDBCEvent.DBCKnockout): void; - - // ============ Forge & Minecraft RAW Hooks ============ - function onCNPCNaturalSpawn(ICustomNPCsEvent: ICustomNPCsEvent.CNPCNaturalSpawnEvent): void; - - function anvilUpdateEvent(ForgeEvent: net.minecraftforge.event.AnvilUpdateEvent): void; - function tickEventPlayerTickEvent(ForgeEvent: cpw.mods.fml.common.gameevent.TickEvent.PlayerTickEvent): void; - function tickEventWorldTickEvent(ForgeEvent: cpw.mods.fml.common.gameevent.TickEvent.WorldTickEvent): void; - function tickEventServerTickEvent(ForgeEvent: cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent): void; - function commandEvent(ForgeEvent: net.minecraftforge.event.CommandEvent): void; - function serverChatEvent(ForgeEvent: net.minecraftforge.event.ServerChatEvent): void; - function itemTossEvent(ForgeEvent: net.minecraftforge.event.entity.item.ItemTossEvent): void; - function livingHurtEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingHurtEvent): void; - function fillBucketEvent(ForgeEvent: net.minecraftforge.event.entity.player.FillBucketEvent): void; - - function inputEventKeyInputEvent(ForgeEvent: cpw.mods.fml.common.gameevent.InputEvent.KeyInputEvent): void; - function inputEventMouseInputEvent(ForgeEvent: cpw.mods.fml.common.gameevent.InputEvent.MouseInputEvent): void; - function playerEventPlayerChangedDimensionEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerChangedDimensionEvent): void; - function playerEventPlayerRespawnEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerRespawnEvent): void; - function playerEventPlayerLoggedOutEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedOutEvent): void; - function playerEventPlayerLoggedInEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.PlayerLoggedInEvent): void; - function playerEventItemSmeltedEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.ItemSmeltedEvent): void; - function playerEventItemCraftedEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.ItemCraftedEvent): void; - function playerEventItemPickupEvent(ForgeEvent: cpw.mods.fml.common.gameevent.PlayerEvent.ItemPickupEvent): void; - function fuelBurnTimeEvent(ForgeEvent: net.minecraftforge.event.FuelBurnTimeEvent): void; - function potionBrewEventPost(ForgeEvent: net.minecraftforge.event.brewing.PotionBrewEvent.Post): void; - function potionBrewEventPre(ForgeEvent: net.minecraftforge.event.brewing.PotionBrewEvent.Pre): void; - function potionBrewedEvent(ForgeEvent: net.minecraftforge.event.brewing.PotionBrewedEvent): void; - function entityEventEnteringChunk(ForgeEvent: net.minecraftforge.event.entity.EntityEvent.EnteringChunk): void; - function entityEventCanUpdate(ForgeEvent: net.minecraftforge.event.entity.EntityEvent.CanUpdate): void; - function entityJoinWorldEvent(ForgeEvent: net.minecraftforge.event.entity.EntityJoinWorldEvent): void; - function entityStruckByLightningEvent(ForgeEvent: net.minecraftforge.event.entity.EntityStruckByLightningEvent): void; - function playSoundAtEntityEvent(ForgeEvent: net.minecraftforge.event.entity.PlaySoundAtEntityEvent): void; - function itemEvent(ForgeEvent: net.minecraftforge.event.entity.item.ItemEvent): void; - function itemExpireEvent(ForgeEvent: net.minecraftforge.event.entity.item.ItemExpireEvent): void; - function enderTeleportEvent(ForgeEvent: net.minecraftforge.event.entity.living.EnderTeleportEvent): void; - function livingAttackEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingAttackEvent): void; - function livingDeathEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingDeathEvent): void; - function livingDropsEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingDropsEvent): void; - function livingEventLivingJumpEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingEvent.LivingJumpEvent): void; - function livingFallEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingFallEvent): void; - function livingHealEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingHealEvent): void; - function livingPackSizeEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingPackSizeEvent): void; - function livingSetAttackTargetEvent(ForgeEvent: net.minecraftforge.event.entity.living.LivingSetAttackTargetEvent): void; - function livingSpawnEventAllowDespawn(ForgeEvent: net.minecraftforge.event.entity.living.LivingSpawnEvent.AllowDespawn): void; - function livingSpawnEventSpecialSpawn(ForgeEvent: net.minecraftforge.event.entity.living.LivingSpawnEvent.SpecialSpawn): void; - function livingSpawnEventCheckSpawn(ForgeEvent: net.minecraftforge.event.entity.living.LivingSpawnEvent.CheckSpawn): void; - function zombieEventSummonAidEvent(ForgeEvent: net.minecraftforge.event.entity.living.ZombieEvent.SummonAidEvent): void; - function minecartCollisionEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; - function minecartEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; - function minecartInteractEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; - function minecartUpdateEvent(ForgeEvent: cpw.mods.fml.common.eventhandler.Event): void; - function achievementEvent(ForgeEvent: net.minecraftforge.event.entity.player.AchievementEvent): void; - function anvilRepairEvent(ForgeEvent: net.minecraftforge.event.entity.player.AnvilRepairEvent): void; - function arrowLooseEvent(ForgeEvent: net.minecraftforge.event.entity.player.ArrowLooseEvent): void; - function arrowNockEvent(ForgeEvent: net.minecraftforge.event.entity.player.ArrowNockEvent): void; - function attackEntityEvent(ForgeEvent: net.minecraftforge.event.entity.player.AttackEntityEvent): void; - function bonemealEvent(ForgeEvent: net.minecraftforge.event.entity.player.BonemealEvent): void; - function entityInteractEvent(ForgeEvent: net.minecraftforge.event.entity.player.EntityInteractEvent): void; - function entityItemPickupEvent(ForgeEvent: net.minecraftforge.event.entity.player.EntityItemPickupEvent): void; - function playerDestroyItemEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerDestroyItemEvent): void; - function playerDropsEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerDropsEvent): void; - function playerEventSaveToFile(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.SaveToFile): void; - function playerEventLoadFromFile(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.LoadFromFile): void; - function playerEventStopTracking(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.StopTracking): void; - function playerEventStartTracking(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.StartTracking): void; - function playerEventClone(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.Clone): void; - function playerEventNameFormat(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.NameFormat): void; - function playerEventBreakSpeed(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.BreakSpeed): void; - function playerEventHarvestCheck(ForgeEvent: net.minecraftforge.event.entity.player.PlayerEvent.HarvestCheck): void; - function playerFlyableFallEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerFlyableFallEvent): void; - function playerOpenContainerEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerOpenContainerEvent): void; - function playerPickupXpEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerPickupXpEvent): void; - function playerSleepInBedEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerSleepInBedEvent): void; - function playerUseItemEventFinish(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Finish): void; - function playerUseItemEventStop(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Stop): void; - function playerUseItemEventTick(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Tick): void; - function playerUseItemEventStart(ForgeEvent: net.minecraftforge.event.entity.player.PlayerUseItemEvent.Start): void; - function playerWakeUpEvent(ForgeEvent: net.minecraftforge.event.entity.player.PlayerWakeUpEvent): void; - function useHoeEvent(ForgeEvent: net.minecraftforge.event.entity.player.UseHoeEvent): void; - function blockEventMultiPlaceEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.MultiPlaceEvent): void; - function blockEventPlaceEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.PlaceEvent): void; - function blockEventBreakEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.BreakEvent): void; - function blockEventHarvestDropsEvent(ForgeEvent: net.minecraftforge.event.world.BlockEvent.HarvestDropsEvent): void; - function explosionEventDetonate(ForgeEvent: net.minecraftforge.event.world.explosion.ExplosionEvent.Detonate): void; - function explosionEventStart(ForgeEvent: net.minecraftforge.event.world.explosion.ExplosionEvent.Start): void; - function noteBlockEventChange(ForgeEvent: net.minecraftforge.event.world.note.NoteBlockEvent.Change): void; - function noteBlockEventPlay(ForgeEvent: net.minecraftforge.event.world.note.NoteBlockEvent.Play): void; - function worldEventCreateSpawnPosition(ForgeEvent: net.minecraftforge.event.world.WorldEvent.CreateSpawnPosition): void; - function worldEventSave(ForgeEvent: net.minecraftforge.event.world.WorldEvent.Save): void; - function worldEventUnload(ForgeEvent: net.minecraftforge.event.world.WorldEvent.Unload): void; - function worldEventLoad(ForgeEvent: net.minecraftforge.event.world.WorldEvent.Load): void; - - // ============ Other Systems ============ - function scriptCommand(ICustomNPCsEvent: ICustomNPCsEvent.ScriptedCommandEvent): void; - - function partyQuestCompleted(IPartyEvent: IPartyEvent.PartyQuestCompletedEvent): void; - function partyQuestSet(IPartyEvent: IPartyEvent.PartyQuestSetEvent): void; - function partyQuestTurnedIn(IPartyEvent: IPartyEvent.PartyQuestTurnedInEvent): void; - function partyInvite(IPartyEvent: IPartyEvent.PartyInviteEvent): void; - function partyKick(IPartyEvent: IPartyEvent.PartyKickEvent): void; - function partyLeave(IPartyEvent: IPartyEvent.PartyLeaveEvent): void; - function partyDisband(IPartyEvent: IPartyEvent.PartyDisbandEvent): void; - - function animationStart(IAnimationEvent: IAnimationEvent.Started): void; - function animationEnd(IAnimationEvent: IAnimationEvent.Ended): void; - function frameEnter(IAnimationEvent: IAnimationEvent.IFrameEvent): void; - function frameExit(IAnimationEvent: IAnimationEvent.IFrameEvent): void; - - function profileChange(IPlayerEvent: IPlayerEvent.ProfileEvent.Changed): void; - function profileRemove(IPlayerEvent: IPlayerEvent.ProfileEvent.Removed): void; - function profileCreate(IPlayerEvent: IPlayerEvent.ProfileEvent.Create): void; - function onEffectAdd(IPlayerEvent: IPlayerEvent.EffectEvent.Added): void; - function onEffectTick(IPlayerEvent: IPlayerEvent.EffectEvent.Ticked): void; - function onEffectRemove(IPlayerEvent: IPlayerEvent.EffectEvent.Removed): void; -} - -export { }; diff --git a/src/main/resources/assets/customnpcs/api/index.d.ts b/src/main/resources/assets/customnpcs/api/index.d.ts deleted file mode 100644 index 43c76fc9d..000000000 --- a/src/main/resources/assets/customnpcs/api/index.d.ts +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Centralized global declarations for CustomNPC+ scripting in VSCode. - * - * This file combines: - * 1. Type aliases for all generated interfaces (so you can use INpcEvent, IPlayer, etc. without imports) - * 2. Ambient function declarations for handler names (so VSCode infers the event type by function name) - * - * Include this file in jsconfig.json so all .js scripts see these globals without per-file imports. - */ - -declare global { - // ============================================================================ - // TYPE ALIASES - Make all interfaces available globally in JS scripts - // ============================================================================ - - type AbstractNpcAPI = import('./noppes/npcs/api/AbstractNpcAPI').AbstractNpcAPI; - type BlockPos = import('./net/minecraft/util/math/BlockPos').BlockPos; - type IAction = import('./noppes/npcs/api/handler/data/IAction').IAction; - type IActionChain = import('./noppes/npcs/api/handler/data/IActionChain').IActionChain; - type IActionListener = import('./noppes/npcs/api/handler/data/IActionListener').IActionListener; - type IActionManager = import('./noppes/npcs/api/handler/IActionManager').IActionManager; - type IActionQueue = import('./noppes/npcs/api/handler/data/IActionQueue').IActionQueue; - type IAnimal = import('./noppes/npcs/api/entity/IAnimal').IAnimal; - type IAnimatable = import('./noppes/npcs/api/entity/IAnimatable').IAnimatable; - type IAnimation = import('./noppes/npcs/api/handler/data/IAnimation').IAnimation; - type IAnimationData = import('./noppes/npcs/api/handler/data/IAnimationData').IAnimationData; - type IAnimationEvent = import('./noppes/npcs/api/event/IAnimationEvent').IAnimationEvent; - type IAnimationHandler = import('./noppes/npcs/api/handler/IAnimationHandler').IAnimationHandler; - type IAnvilRecipe = import('./noppes/npcs/api/handler/data/IAnvilRecipe').IAnvilRecipe; - type IArrow = import('./noppes/npcs/api/entity/IArrow').IArrow; - type IAttributeDefinition = import('./noppes/npcs/api/handler/data/IAttributeDefinition').IAttributeDefinition; - type IAttributeHandler = import('./noppes/npcs/api/handler/IAttributeHandler').IAttributeHandler; - type IAvailability = import('./noppes/npcs/api/handler/data/IAvailability').IAvailability; - type IBlock = import('./noppes/npcs/api/IBlock').IBlock; - type IBlockEvent = import('./noppes/npcs/api/event/IBlockEvent').IBlockEvent; - type IBlockScripted = import('./noppes/npcs/api/block/IBlockScripted').IBlockScripted; - type IBlockState = import('./net/minecraft/block/state/IBlockState').IBlockState; - type IButton = import('./noppes/npcs/api/gui/IButton').IButton; - type ICloneHandler = import('./noppes/npcs/api/handler/ICloneHandler').ICloneHandler; - type ICommand = import('./noppes/npcs/api/ICommand').ICommand; - type IConditionalAction = import('./noppes/npcs/api/handler/data/actions/IConditionalAction').IConditionalAction; - type IContainer = import('./noppes/npcs/api/IContainer').IContainer; - type ICustomAttribute = import('./noppes/npcs/api/handler/data/ICustomAttribute').ICustomAttribute; - type ICustomEffect = import('./noppes/npcs/api/handler/data/ICustomEffect').ICustomEffect; - type ICustomEffectHandler = import('./noppes/npcs/api/handler/ICustomEffectHandler').ICustomEffectHandler; - type ICustomGui = import('./noppes/npcs/api/gui/ICustomGui').ICustomGui; - type ICustomGuiComponent = import('./noppes/npcs/api/gui/ICustomGuiComponent').ICustomGuiComponent; - type ICustomGuiEvent = import('./noppes/npcs/api/event/ICustomGuiEvent').ICustomGuiEvent; - type ICustomNPCsEvent = import('./noppes/npcs/api/event/ICustomNPCsEvent').ICustomNPCsEvent; - type ICustomNpc = import('./noppes/npcs/api/entity/ICustomNpc').ICustomNpc; - type ICustomOverlay = import('./noppes/npcs/api/overlay/ICustomOverlay').ICustomOverlay; - type ICustomOverlayComponent = import('./noppes/npcs/api/overlay/ICustomOverlayComponent').ICustomOverlayComponent; - type IDBCPlayer = import('./noppes/npcs/api/entity/IDBCPlayer').IDBCPlayer; - type IDamageSource = import('./noppes/npcs/api/IDamageSource').IDamageSource; - type IDialog = import('./noppes/npcs/api/handler/data/IDialog').IDialog; - type IDialogCategory = import('./noppes/npcs/api/handler/data/IDialogCategory').IDialogCategory; - type IDialogEvent = import('./noppes/npcs/api/event/IDialogEvent').IDialogEvent; - type IDialogHandler = import('./noppes/npcs/api/handler/IDialogHandler').IDialogHandler; - type IDialogImage = import('./noppes/npcs/api/handler/data/IDialogImage').IDialogImage; - type IDialogOption = import('./noppes/npcs/api/handler/data/IDialogOption').IDialogOption; - type IEntity = import('./noppes/npcs/api/entity/IEntity').IEntity; - type IEntityItem = import('./noppes/npcs/api/entity/IEntityItem').IEntityItem; - type IEntityLiving = import('./noppes/npcs/api/entity/IEntityLiving').IEntityLiving; - type IFaction = import('./noppes/npcs/api/handler/data/IFaction').IFaction; - type IFactionEvent = import('./noppes/npcs/api/event/IFactionEvent').IFactionEvent; - type IFactionHandler = import('./noppes/npcs/api/handler/IFactionHandler').IFactionHandler; - type IFishHook = import('./noppes/npcs/api/entity/IFishHook').IFishHook; - type IForgeEvent = import('./noppes/npcs/api/event/IForgeEvent').IForgeEvent; - type IFrame = import('./noppes/npcs/api/handler/data/IFrame').IFrame; - type IFramePart = import('./noppes/npcs/api/handler/data/IFramePart').IFramePart; - type IItemArmor = import('./noppes/npcs/api/item/IItemArmor').IItemArmor; - type IItemBlock = import('./noppes/npcs/api/item/IItemBlock').IItemBlock; - type IItemBook = import('./noppes/npcs/api/item/IItemBook').IItemBook; - type IItemCustom = import('./noppes/npcs/api/item/IItemCustom').IItemCustom; - type IItemCustomizable = import('./noppes/npcs/api/item/IItemCustomizable').IItemCustomizable; - type IItemEvent = import('./noppes/npcs/api/event/IItemEvent').IItemEvent; - type IItemLinked = import('./noppes/npcs/api/item/IItemLinked').IItemLinked; - type IItemSlot = import('./noppes/npcs/api/gui/IItemSlot').IItemSlot; - type IItemStack = import('./noppes/npcs/api/item/IItemStack').IItemStack; - type IJob = import('./noppes/npcs/api/jobs/IJob').IJob; - type IJobBard = import('./noppes/npcs/api/jobs/IJobBard').IJobBard; - type IJobConversation = import('./noppes/npcs/api/jobs/IJobConversation').IJobConversation; - type IJobFollower = import('./noppes/npcs/api/jobs/IJobFollower').IJobFollower; - type IJobGuard = import('./noppes/npcs/api/jobs/IJobGuard').IJobGuard; - type IJobHealer = import('./noppes/npcs/api/jobs/IJobHealer').IJobHealer; - type IJobItemGiver = import('./noppes/npcs/api/jobs/IJobItemGiver').IJobItemGiver; - type IJobSpawner = import('./noppes/npcs/api/jobs/IJobSpawner').IJobSpawner; - type ILinkedItemEvent = import('./noppes/npcs/api/event/ILinkedItemEvent').ILinkedItemEvent; - type IMarketCategory = import('./noppes/npcs/api/handler/data/IMarketCategory').IMarketCategory; - type IMessage = import('./noppes/npcs/api/handler/data/IMessage').IMessage; - type IModel = import('./noppes/npcs/api/handler/data/IModel').IModel; - type IModelData = import('./noppes/npcs/api/handler/data/IModelData').IModelData; - type IModelPart = import('./noppes/npcs/api/handler/data/IModelPart').IModelPart; - type IModelRotate = import('./noppes/npcs/api/handler/data/IModelRotate').IModelRotate; - type IModelRotatePart = import('./noppes/npcs/api/handler/data/IModelRotatePart').IModelRotatePart; - type IMonster = import('./noppes/npcs/api/entity/IMonster').IMonster; - type INbtTagCompound = import('./noppes/npcs/api/INbt').INbtTagCompound; - type INpcEvent = import('./noppes/npcs/api/event/INpcEvent').INpcEvent; - type IParallel = import('./noppes/npcs/api/handler/data/IParallel').IParallel; - type IParallelLine = import('./noppes/npcs/api/handler/data/IParallelLine').IParallelLine; - type IPartyEvent = import('./noppes/npcs/api/event/IPartyEvent').IPartyEvent; - type IPartyHandler = import('./noppes/npcs/api/handler/IPartyHandler').IPartyHandler; - type IPayloadCompound = import('./noppes/npcs/api/IPayloadCompound').IPayloadCompound; - type IPixelmon = import('./noppes/npcs/api/entity/IPixelmon').IPixelmon; - type IPixelmonPlayerData = import('./noppes/npcs/api/IPixelmonPlayerData').IPixelmonPlayerData; - type IPlayer = import('./noppes/npcs/api/entity/IPlayer').IPlayer; - type IPlayerEvent = import('./noppes/npcs/api/event/IPlayerEvent').IPlayerEvent; - type IPos = import('./noppes/npcs/api/IPos').IPos; - type IProjectile = import('./noppes/npcs/api/entity/IProjectile').IProjectile; - type IProjectileEvent = import('./noppes/npcs/api/event/IProjectileEvent').IProjectileEvent; - type IQuestEvent = import('./noppes/npcs/api/event/IQuestEvent').IQuestEvent; - type IQuestHandler = import('./noppes/npcs/api/handler/IQuestHandler').IQuestHandler; - type IRecipeEvent = import('./noppes/npcs/api/event/IRecipeEvent').IRecipeEvent; - type IRole = import('./noppes/npcs/api/roles/IRole').IRole; - type IRoleArmor = import('./noppes/npcs/api/roles/IRoleArmor').IRoleArmor; - type IRoleDye = import('./noppes/npcs/api/roles/IRoleDye').IRoleDye; - type IRoleHair = import('./noppes/npcs/api/roles/IRoleHair').IRoleHair; - type IRoleHat = import('./noppes/npcs/api/roles/IRoleHat').IRoleHat; - type IRoleHeld = import('./noppes/npcs/api/roles/IRoleHeld').IRoleHeld; - type IRoleLeft = import('./noppes/npcs/api/roles/IRoleLeft').IRoleLeft; - type IRoleRight = import('./noppes/npcs/api/roles/IRoleRight').IRoleRight; - type IRoleSkinPart = import('./noppes/npcs/api/roles/IRoleSkinPart').IRoleSkinPart; - type IRoleSmile = import('./noppes/npcs/api/roles/IRoleSmile').IRoleSmile; - type IScoreboard = import('./noppes/npcs/api/scoreboard/IScoreboard').IScoreboard; - type IScoreboardFont = import('./noppes/npcs/api/scoreboard/IScoreboardFont').IScoreboardFont; - type IScreenSize = import('./noppes/npcs/api/IScreenSize').IScreenSize; - type ISerializable = import('./noppes/npcs/api/ISerializable').ISerializable; - type IShapedRecipe = import('./noppes/npcs/api/IShapedRecipe').IShapedRecipe; - type IShapelessRecipe = import('./noppes/npcs/api/IShapelessRecipe').IShapelessRecipe; - type IShop = import('./noppes/npcs/api/handler/data/IShop').IShop; - type IShopCategory = import('./noppes/npcs/api/handler/data/IShopCategory').IShopCategory; - type IShopItem = import('./noppes/npcs/api/handler/data/IShopItem').IShopItem; - type ISkinOverlay = import('./noppes/npcs/api/ISkinOverlay').ISkinOverlay; - type ISlot = import('./noppes/npcs/api/handler/data/ISlot').ISlot; - type ISound = import('./noppes/npcs/api/handler/data/ISound').ISound; - type ITag = import('./noppes/npcs/api/handler/data/ITag').ITag; - type ITagHandler = import('./noppes/npcs/api/handler/ITagHandler').ITagHandler; - type ITextField = import('./noppes/npcs/api/gui/ITextField').ITextField; - type ITextPlane = import('./noppes/npcs/api/block/ITextPlane').ITextPlane; - type ITexturedRect = import('./noppes/npcs/api/gui/ITexturedRect').ITexturedRect; - type IThrowable = import('./noppes/npcs/api/entity/IThrowable').IThrowable; - type ITileEntity = import('./noppes/npcs/api/ITileEntity').ITileEntity; - type ITimers = import('./noppes/npcs/api/ITimers').ITimers; - type ITransportCategory = import('./noppes/npcs/api/handler/data/ITransportCategory').ITransportCategory; - type ITransportHandler = import('./noppes/npcs/api/handler/ITransportHandler').ITransportHandler; - type ITransportLocation = import('./noppes/npcs/api/handler/data/ITransportLocation').ITransportLocation; - type IVillager = import('./noppes/npcs/api/entity/IVillager').IVillager; - type IWorld = import('./noppes/npcs/api/IWorld').IWorld; - type IntHashMap = import('./net/minecraft/util/math/IntHashMap').IntHashMap; - type Vec3i = import('./net/minecraft/util/Vec3i').Vec3i; - - // ============================================================================ - // GLOBAL API INSTANCE - // ============================================================================ - - const API: import('./noppes/npcs/api/AbstractNpcAPI').AbstractNpcAPI; - - // ============================================================================ - // NESTED INTERFACES - Allow autocomplete like INpcEvent.InitEvent - // ============================================================================ - - namespace INpcEvent { - interface CollideEvent extends INpcEvent {} - interface DamagedEvent extends INpcEvent {} - interface RangedLaunchedEvent extends INpcEvent {} - interface MeleeAttackEvent extends INpcEvent {} - interface SwingEvent extends INpcEvent {} - interface KilledEntityEvent extends INpcEvent {} - interface DiedEvent extends INpcEvent {} - interface InteractEvent extends INpcEvent {} - interface DialogEvent extends INpcEvent {} - interface TimerEvent extends INpcEvent {} - interface TargetEvent extends INpcEvent {} - interface TargetLostEvent extends INpcEvent {} - interface DialogClosedEvent extends INpcEvent {} - interface UpdateEvent extends INpcEvent {} - interface InitEvent extends INpcEvent {} - } - - namespace IPlayerEvent { - interface ChatEvent extends IPlayerEvent {} - interface KeyPressedEvent extends IPlayerEvent {} - interface MouseClickedEvent extends IPlayerEvent {} - interface PickupXPEvent extends IPlayerEvent {} - interface LevelUpEvent extends IPlayerEvent {} - interface LogoutEvent extends IPlayerEvent {} - interface LoginEvent extends IPlayerEvent {} - interface RespawnEvent extends IPlayerEvent {} - interface ChangedDimension extends IPlayerEvent {} - interface TimerEvent extends IPlayerEvent {} - interface AttackedEvent extends IPlayerEvent {} - interface DamagedEvent extends IPlayerEvent {} - interface LightningEvent extends IPlayerEvent {} - interface SoundEvent extends IPlayerEvent {} - interface FallEvent extends IPlayerEvent {} - interface JumpEvent extends IPlayerEvent {} - interface KilledEntityEvent extends IPlayerEvent {} - interface DiedEvent extends IPlayerEvent {} - interface RangedLaunchedEvent extends IPlayerEvent {} - interface AttackEvent extends IPlayerEvent {} - interface DamagedEntityEvent extends IPlayerEvent {} - interface ContainerClosed extends IPlayerEvent {} - interface ContainerOpen extends IPlayerEvent {} - interface PickUpEvent extends IPlayerEvent {} - interface DropEvent extends IPlayerEvent {} - interface TossEvent extends IPlayerEvent {} - interface InteractEvent extends IPlayerEvent {} - interface RightClickEvent extends IPlayerEvent {} - interface UpdateEvent extends IPlayerEvent {} - interface InitEvent extends IPlayerEvent {} - interface StartUsingItem extends IPlayerEvent {} - interface UsingItem extends IPlayerEvent {} - interface StopUsingItem extends IPlayerEvent {} - interface FinishUsingItem extends IPlayerEvent {} - interface BreakEvent extends IPlayerEvent {} - interface UseHoeEvent extends IPlayerEvent {} - interface WakeUpEvent extends IPlayerEvent {} - interface SleepEvent extends IPlayerEvent {} - interface AchievementEvent extends IPlayerEvent {} - interface FillBucketEvent extends IPlayerEvent {} - interface BonemealEvent extends IPlayerEvent {} - interface RangedChargeEvent extends IPlayerEvent {} - interface EffectEvent extends IPlayerEvent {} - interface ProfileEvent extends IPlayerEvent {} - } - - namespace IProjectileEvent { - interface UpdateEvent extends IProjectileEvent {} - interface ImpactEvent extends IProjectileEvent {} - } - - namespace IQuestEvent { - interface QuestStartEvent extends IQuestEvent {} - interface QuestCompletedEvent extends IQuestEvent {} - interface QuestTurnedInEvent extends IQuestEvent {} - } - - namespace IDialogEvent { - interface DialogClosed extends IDialogEvent {} - } - - namespace IFactionEvent { - interface FactionPointsEvent extends IFactionEvent {} - } - - namespace IPartyEvent { - interface PartyQuestCompletedEvent extends IPartyEvent {} - interface PartyQuestSetEvent extends IPartyEvent {} - interface PartyQuestTurnedInEvent extends IPartyEvent {} - interface PartyInviteEvent extends IPartyEvent {} - interface PartyKickEvent extends IPartyEvent {} - interface PartyLeaveEvent extends IPartyEvent {} - interface PartyDisbandEvent extends IPartyEvent {} - } - - namespace IBlockEvent { - interface InitEvent extends IBlockEvent {} - interface InteractEvent extends IBlockEvent {} - interface UpdateEvent extends IBlockEvent {} - interface TimerEvent extends IBlockEvent {} - } - - namespace ICustomGuiEvent { - interface CloseEvent extends ICustomGuiEvent {} - interface ButtonEvent extends ICustomGuiEvent {} - interface SlotEvent extends ICustomGuiEvent {} - interface SlotClickEvent extends ICustomGuiEvent {} - interface ScrollEvent extends ICustomGuiEvent {} - } - - namespace IAnimationEvent { - interface Started extends IAnimationEvent {} - interface Ended extends IAnimationEvent {} - interface IFrameEvent extends IAnimationEvent {} - } - - namespace IItemEvent { - interface InitEvent extends IItemEvent {} - interface UpdateEvent extends IItemEvent {} - interface TossedEvent extends IItemEvent {} - interface PickedUpEvent extends IItemEvent {} - interface SpawnEvent extends IItemEvent {} - interface InteractEvent extends IItemEvent {} - interface RightClickEvent extends IItemEvent {} - interface AttackEvent extends IItemEvent {} - interface StartUsingItem extends IItemEvent {} - interface UsingItem extends IItemEvent {} - interface StopUsingItem extends IItemEvent {} - interface FinishUsingItem extends IItemEvent {} - } - - // ============================================================================ - // AMBIENT HANDLER FUNCTIONS - Defined in hooks.d.ts - // ============================================================================ - // For type-safe handlers with overloads (parameter names determine event type), - // see hooks.d.ts which includes declare function overloads. - // - // Example: - // function damaged(INpcEvent: INpcEvent.DamagedEvent) { INpcEvent. } // infers INpcEvent.DamagedEvent - // function damaged(IPlayerEvent: IPlayerEvent.DamagedEvent) { IPlayerEvent. } // infers IPlayerEvent.DamagedEvent -} - -export {}; diff --git a/src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts b/src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts deleted file mode 100644 index a0695de2f..000000000 --- a/src/main/resources/assets/customnpcs/api/minecraft-raw.d.ts +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Raw Minecraft 1.7.10 Type Definitions (MCP Names) - * These represent the internal deobfuscated Minecraft classes. - */ - -declare global { - namespace net.minecraft { - export namespace block { - export interface Block { - /** @returns The unlocalized name of the block */ - getUnlocalizedName(): string; - /** @returns The localized name of the block */ - getLocalizedName(): string; - /** @returns Light opacity of the block */ - getLightOpacity(): number; - /** @returns Light value of the block */ - getLightValue(): number; - } - } - - export namespace damage { - export interface DamageSource { - /** The type of damage (e.g. "player", "fall", "magic") */ - damageType: string; - /** @returns True if damage bypasses armor */ - isUnblockable(): boolean; - /** @returns True if damage is absolute (bypasses potions etc) */ - isDamageAbsolute(): boolean; - /** @returns The entity that caused the damage */ - getEntity(): net.minecraft.entity.Entity; - /** @returns The entity that directly dealt the damage (e.g. arrow) */ - getSourceOfDamage(): net.minecraft.entity.Entity; - } - } - - export namespace entity { - /** Base class for all entities in Minecraft 1.7.10 */ - export interface Entity { - /** The entity's unique ID */ - entityId: number; - /** The world the entity is in */ - worldObj: net.minecraft.world.World; - /** X position */ - posX: number; - /** Y position */ - posY: number; - /** Z position */ - posZ: number; - /** X motion */ - motionX: number; - /** Y motion */ - motionY: number; - /** Z motion */ - motionZ: number; - /** Rotation yaw */ - rotationYaw: number; - /** Rotation pitch */ - rotationPitch: number; - /** Entity width */ - width: number; - /** Entity height */ - height: number; - /** Whether the entity is on the ground */ - onGround: boolean; - /** Whether the entity is in water */ - isInWeb: boolean; - /** Ticks the entity has been alive */ - ticksExisted: number; - /** Remaining air */ - air: number; - /** Damage sustained from falling */ - fallDistance: number; - - /** Gets a unique string key for this entity type */ - getEntityString(): string; - /** Sets the entity's position */ - setPosition(x: number, y: number, z: number): void; - /** Sets the entity's rotation */ - setRotation(yaw: number, pitch: number): void; - /** Marks the entity for removal */ - setDead(): void; - /** @returns True if the entity is burning */ - isBurning(): boolean; - /** @returns True if the entity is sneaking */ - isSneaking(): boolean; - /** @returns True if the entity is sprinting */ - isSprinting(): boolean; - /** @returns True if the entity is invisible */ - isInvisible(): boolean; - } - - export interface EntityLivingBase extends Entity { - /** @returns Current health */ - getHealth(): number; - /** Sets the entity's health */ - setHealth(health: number): void; - /** @returns Max health */ - getMaxHealth(): number; - /** @returns True if entity is child */ - isChild(): boolean; - /** Sets entity on fire */ - setFire(seconds: number): void; - } - - export interface EntityLiving extends EntityLivingBase { - /** Sets the attack target */ - setAttackTarget(target: net.minecraft.entity.EntityLivingBase): void; - /** @returns The current attack target */ - getAttackTarget(): net.minecraft.entity.EntityLivingBase; - /** @returns True if the entity can pick up loot */ - canPickUpLoot(): boolean; - /** Sets if the entity can pick up loot */ - setCanPickUpLoot(can: boolean): void; - } - - export interface EntityItem extends Entity { - /** The ItemStack contained in this entity */ - getEntityItem(): net.minecraft.item.ItemStack; - /** Sets the ItemStack contained in this entity */ - setEntityItemStack(stack: net.minecraft.item.ItemStack): void; - } - - export interface EntityZombie extends EntityLiving { - /** @returns True if the zombie is a child */ - isChild(): boolean; - /** Sets if the zombie is a child */ - setChild(child: boolean): void; - } - - export namespace player { - /** Representation of a Player in Minecraft 1.7.10 */ - export interface EntityPlayer extends net.minecraft.entity.Entity { - /** The player's inventory */ - inventory: net.minecraft.entity.player.InventoryPlayer; - /** The player's experience level */ - experienceLevel: number; - /** Total experience points */ - experienceTotal: number; - /** Progress to next level (0.0 to 1.0) */ - experience: number; - /** Player capabilities (flying, creative, etc.) */ - capabilities: net.minecraft.entity.player.PlayerCapabilities; - /** Food stats (hunger, saturation) */ - foodStats: any; - /** The player's dimension ID */ - dimension: number; - - /** Sends a chat message to the player */ - addChatMessage(component: any): void; - /** @returns The display name of the player */ - getDisplayName(): string; - /** @returns The actual username/command sender name */ - getCommandSenderName(): string; - /** Closes any open GUI */ - closeScreen(): void; - } - - export interface InventoryPlayer { - /** Current selected slot index (0-8) */ - currentItem: number; - /** Main inventory array (36 slots) */ - mainInventory: net.minecraft.item.ItemStack[]; - /** Armor inventory array (4 slots) */ - armorInventory: net.minecraft.item.ItemStack[]; - - /** Gets the currently held item */ - getCurrentItem(): net.minecraft.item.ItemStack; - /** Consumes 1 of the item in the specified slot */ - consumeInventoryItem(item: any): boolean; - /** Checks if the player has a specific item */ - hasItem(item: any): boolean; - } - - export interface PlayerCapabilities { - /** Whether the player is invulnerable */ - disableDamage: boolean; - /** Whether the player is flying */ - isFlying: boolean; - /** Whether the player can fly */ - allowFlying: boolean; - /** Whether the player is in creative mode */ - isCreativeMode: boolean; - /** Flying speed */ - flySpeed: number; - /** Walking speed */ - walkSpeed: number; - } - } - } - - export namespace world { - /** Representation of the World in Minecraft 1.7.10 */ - export interface World { - /** World provider (dimension info) */ - provider: net.minecraft.world.WorldProvider; - /** List of all entities in the world */ - loadedEntityList: net.minecraft.entity.Entity[]; - /** List of all players in the world */ - playerEntities: net.minecraft.entity.player.EntityPlayer[]; - /** True if this is a client-side world */ - isRemote: boolean; - /** World time in ticks */ - worldTime: number; - /** The world's difficulty (0-3) */ - difficultySetting: number; - - /** @returns The block at the given coordinates */ - getBlock(x: number, y: number, z: number): net.minecraft.block.Block; - /** @returns The metadata of the block at the given coordinates */ - getBlockMetadata(x: number, y: number, z: number): number; - /** Sets the block and metadata at the given coordinates */ - setBlock(x: number, y: number, z: number, block: net.minecraft.block.Block, metadata: number, flags: number): boolean; - /** Spawns an entity in the world */ - spawnEntityInWorld(entity: net.minecraft.entity.Entity): boolean; - /** Plays a sound at the given coordinates */ - playSoundEffect(x: number, y: number, z: number, name: string, volume: number, pitch: number): void; - } - - export interface WorldProvider { - /** Dimension ID (0 = Overworld, -1 = Nether, 1 = End) */ - dimensionId: number; - /** Name of the dimension type */ - dimensionName: string; - /** Whether this dimension has a sky */ - hasSkyLight: boolean; - } - } - - export namespace item { - /** Representation of an Item stack in Minecraft 1.7.10 */ - export interface ItemStack { - /** Number of items in the stack */ - stackSize: number; - /** Damage/Metadata value of the item */ - itemDamage: number; - /** The underlying Item object */ - item: any; - /** NBT data of the stack */ - stackTagCompound: any; - - /** @returns The display name of the item */ - getDisplayName(): string; - /** @returns True if the item has NBT data */ - hasTagCompound(): boolean; - /** Copies the item stack */ - copy(): net.minecraft.item.ItemStack; - } - } - - export namespace util { - /** Result of a raytrace in 1.7.10 */ - export interface MovingObjectPosition { - /** 0 = MISS, 1 = BLOCK, 2 = ENTITY */ - typeOfHit: number; - /** X coordinate of the hit block */ - blockX: number; - /** Y coordinate of the hit block */ - blockY: number; - /** Z coordinate of the hit block */ - blockZ: number; - /** Side of the block hit (0-5) */ - sideHit: number; - /** The entity hit, if typeOfHit is ENTITY */ - entityHit: net.minecraft.entity.Entity; - /** The exact hit vector */ - hitVec: net.minecraft.util.Vec3; - } - - export interface Vec3 { - xCoord: number; - yCoord: number; - zCoord: number; - } - } - } -} - -export { }; diff --git a/src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts b/src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts deleted file mode 100644 index a40d27121..000000000 --- a/src/main/resources/assets/customnpcs/api/net/minecraft/block/properties/IProperty.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Generated from Java interface net.minecraft.block.properties.IProperty - * Source: CustomNPC+ JavaDoc (Minecraft 1.7.10) - */ - -export interface IProperty { - /** Returns the property name */ - getName(): string; - - /** Returns the allowed values for this property */ - getAllowedValues(): Array; - - /** Returns the runtime value class for this property (approximation) */ - getValueClass(): Function; - - /** Returns the name for a given value */ - getName(value: T): string; -} diff --git a/src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts b/src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts deleted file mode 100644 index 7361d198a..000000000 --- a/src/main/resources/assets/customnpcs/api/net/minecraft/block/state/IBlockState.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: net.minecraft.block.state - */ - -export interface IBlockState { - - // Methods - getPropertyNames(): Array { - - // Fields - x: number; - y: number; - z: number; - xd: number; - yd: number; - zd: number; - true: any; - false: any; - vec3I: import('./Vec3i').Vec3i; - - // Methods - equals(p_equals_1_: any): boolean; - hashCode(): number; - compareTo(p_compareTo_1_: import('./Vec3i').Vec3i): number; - getX(): number; - getY(): number; - getZ(): number; - getXD(): number; - getYD(): number; - getZD(): number; - crossProduct(vec: import('./Vec3i').Vec3i): i; - toString(): string; - -} diff --git a/src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts b/src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts deleted file mode 100644 index df828d373..000000000 --- a/src/main/resources/assets/customnpcs/api/net/minecraft/util/math/BlockPos.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: net.minecraft.util.math - */ - -export class BlockPos extends Vec3i { - - // Fields - LOGGER: Logger; - ORIGIN: import('./BlockPos').BlockPos; - NUM_X_BITS: number; - NUM_Z_BITS: number; - NUM_Y_BITS: number; - Y_SHIFT: number; - X_SHIFT: number; - X_MASK: number; - Y_MASK: number; - Z_MASK: number; - x: any; - x: any; - n: any; - 0: <<; - i: number; - j: number; - k: number; - blockpos: import('./BlockPos').BlockPos; - blockpos1: import('./BlockPos').BlockPos; - lastReturned: import('./BlockPos').BlockPos; - i: number; - j: number; - k: number; - x: number; - y: number; - z: number; - this: any; - released: boolean; - POOL: Array; - sizeOfObject(obj: any): number; - stopServer(): void; - getCurrentPlayerCount(): number; - getMaxPlayers(): number; - kickAllPlayers(): void; - isHardcore(): boolean; - getFile(path: string): import('../../../java/io/File').File; - getServerOwner(): string; - getFactions(): import('./handler/IFactionHandler').IFactionHandler; - getRecipes(): import('./handler/IRecipeHandler').IRecipeHandler; - getQuests(): import('./handler/IQuestHandler').IQuestHandler; - getDialogs(): import('./handler/IDialogHandler').IDialogHandler; - getClones(): import('./handler/ICloneHandler').ICloneHandler; - getNaturalSpawns(): import('./handler/INaturalSpawnsHandler').INaturalSpawnsHandler; - getProfileHandler(): import('./handler/IProfileHandler').IProfileHandler; - getCustomEffectHandler(): import('./handler/ICustomEffectHandler').ICustomEffectHandler; - getMagicHandler(): import('./handler/IMagicHandler').IMagicHandler; - getPartyHandler(): import('./handler/IPartyHandler').IPartyHandler; - getLocations(): import('./handler/ITransportHandler').ITransportHandler; - getAnimations(): import('./handler/IAnimationHandler').IAnimationHandler; - getAllBiomeNames(): string[]; - createNPC(world: import('./IWorld').IWorld): import('./entity/ICustomNpc').ICustomNpc; - spawnNPC(world: import('./IWorld').IWorld, x: number, y: number, z: number): import('./entity/ICustomNpc').ICustomNpc; - spawnNPC(world: import('./IWorld').IWorld, pos: import('./IPos').IPos): import('./entity/ICustomNpc').ICustomNpc; - getIEntity(entity: any): import('./entity/IEntity').IEntity; - getPlayer(username: string): import('./entity/IPlayer').IPlayer; - getChunkLoadingNPCs(): import('./entity/ICustomNpc').ICustomNpc[]; - getLoadedEntities(): import('./entity/IEntity').IEntity[]; - getIBlock(world: import('./IWorld').IWorld, x: number, y: number, z: number): import('./IBlock').IBlock; - getIBlock(world: import('./IWorld').IWorld, pos: import('./IPos').IPos): import('./IBlock').IBlock; - getITileEntity(world: import('./IWorld').IWorld, pos: import('./IPos').IPos): import('./ITileEntity').ITileEntity; - getITileEntity(world: import('./IWorld').IWorld, x: number, y: number, z: number): import('./ITileEntity').ITileEntity; - getITileEntity(tileEntity: any): import('./ITileEntity').ITileEntity; - getIPos(pos: any): import('./IPos').IPos; - getIPos(x: number, y: number, z: number): import('./IPos').IPos; - getIPos(serializedPos: number): import('./IPos').IPos; - getAllInBox(from: import('./IPos').IPos, to: import('./IPos').IPos, sortByDistance: boolean): import('./IPos').IPos[]; - getAllInBox(from: import('./IPos').IPos, to: import('./IPos').IPos): import('./IPos').IPos[]; - getIContainer(inventory: any): import('./IContainer').IContainer; - getIItemStack(itemStack: any): import('./item/IItemStack').IItemStack; - getIWorld(world: any): import('./IWorld').IWorld; - getIWorld(dimensionId: number): import('./IWorld').IWorld; - getIWorldLoad(dimensionId: number): import('./IWorld').IWorld; - getActionManager(): import('./handler/IActionManager').IActionManager; - getIWorlds(): import('./IWorld').IWorld[]; - getIDamageSource(damageSource: any): import('./IDamageSource').IDamageSource; - getIDamageSource(entity: import('./entity/IEntity').IEntity): import('./IDamageSource').IDamageSource; - events(): import('../../../cpw/mods/fml/common/eventhandler/EventBus').EventBus; - getGlobalDir(): import('../../../java/io/File').File; - getWorldDir(): import('../../../java/io/File').File; - executeCommand(world: import('./IWorld').IWorld, command: string): void; - getRandomName(dictionary: number, gender: number): string; - getINbt(nbt: any): import('./INbt').INbt; - stringToNbt(str: string): import('./INbt').INbt; - getAllServerPlayers(): import('./entity/IPlayer').IPlayer[]; - getPlayerNames(): string[]; - createItemFromNBT(nbt: import('./INbt').INbt): import('./item/IItemStack').IItemStack; - createItem(id: string, damage: number, size: number): import('./item/IItemStack').IItemStack; - playSoundAtEntity(entity: import('./entity/IEntity').IEntity, sound: string, volume: number, pitch: number): void; - playSoundToNearExcept(player: import('./entity/IPlayer').IPlayer, sound: string, volume: number, pitch: number): void; - getMOTD(): string; - setMOTD(motd: string): void; - createParticle(directory: string): import('./IParticle').IParticle; - createEntityParticle(directory: string): import('./IParticle').IParticle; - createSound(directory: string): import('./handler/data/ISound').ISound; - playSound(id: number, sound: import('./handler/data/ISound').ISound): void; - playSound(sound: import('./handler/data/ISound').ISound): void; - stopSound(id: number): void; - pauseSounds(): void; - continueSounds(): void; - stopSounds(): void; - getServerTime(): number; - arePlayerScriptsEnabled(): boolean; - areForgeScriptsEnabled(): boolean; - areGlobalNPCScriptsEnabled(): boolean; - enablePlayerScripts(enable: boolean): void; - enableForgeScripts(enable: boolean): void; - enableGlobalNPCScripts(enable: boolean): void; - createCustomGui(id: number, width: number, height: number, pauseGame: boolean): import('./gui/ICustomGui').ICustomGui; - createCustomOverlay(id: number): import('./overlay/ICustomOverlay').ICustomOverlay; - createSkinOverlay(texture: string): import('./ISkinOverlay').ISkinOverlay; - millisToTime(millis: number): string; - ticksToTime(ticks: number): string; - createAnimation(name: string): import('./handler/data/IAnimation').IAnimation; - createAnimation(name: string, speed: number, smooth: number): import('./handler/data/IAnimation').IAnimation; - createFrame(duration: number): import('./handler/data/IFrame').IFrame; - createFrame(duration: number, speed: number, smooth: number): import('./handler/data/IFrame').IFrame; - createPart(name: string): import('./handler/data/IFramePart').IFramePart; - createPart(name: string, rotation: number[], pivot: number[]): import('./handler/data/IFramePart').IFramePart; - createPart(name: string, rotation: number[], pivot: number[], speed: number, smooth: number): import('./handler/data/IFramePart').IFramePart; - createPart(partId: number): import('./handler/data/IFramePart').IFramePart; - createPart(partId: number, rotation: number[], pivot: number[]): import('./handler/data/IFramePart').IFramePart; - createPart(partId: number, rotation: number[], pivot: number[], speed: number, smooth: number): import('./handler/data/IFramePart').IFramePart; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IBlock.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IBlock.d.ts deleted file mode 100644 index 2dfd7b359..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IBlock.d.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IBlock { - - // Methods - getX(): number; - getY(): number; - getZ(): number; - getPosition(): import('./IPos').IPos; - setPosition(pos: import('./IPos').IPos, world: import('./IWorld').IWorld): boolean; - setPosition(pos: import('./IPos').IPos): boolean; - setPosition(x: number, y: number, z: number, world: import('./IWorld').IWorld): boolean; - setPosition(x: number, y: number, z: number): boolean; - getName(): string; - remove(): void; - isAir(): boolean; - setBlock(blockName: string): import('./IBlock').IBlock; - setBlock(block: import('./IBlock').IBlock): import('./IBlock').IBlock; - isContainer(): boolean; - getContainer(): import('./IContainer').IContainer; - getWorld(): import('./IWorld').IWorld; - hasTileEntity(): boolean; - getTileEntity(): import('./ITileEntity').ITileEntity; - setTileEntity(tileEntity: import('./ITileEntity').ITileEntity): void; - getMCTileEntity(): TileEntity; - getMCBlock(): Block; - getDisplayName(): string; - getTileEntityNBT(): import('./INbt').INbt; - canCollide(maxVolume: number): boolean; - canCollide(): boolean; - setBounds(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): void; - getBlockBoundsMinX(): number; - getBlockBoundsMinY(): number; - getBlockBoundsMinZ(): number; - getBlockBoundsMaxX(): number; - getBlockBoundsMaxY(): number; - getBlockBoundsMaxZ(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ICommand.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ICommand.d.ts deleted file mode 100644 index 8311dd8b6..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ICommand.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface ICommand { - - // Methods - getCommandName(): string; - getCommandUsage(): string; - getPermissionLevel(): number; - setCommandName(commandName: string): void; - setCommandUsage(commandUsage: string): void; - setPermissionLevel(permissionLevel: number): void; - getAliases(): string[]; - addAliases(aliases: ): void; - hasAlias(alias: string): boolean; - removeAlias(alias: string): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IContainer.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IContainer.d.ts deleted file mode 100644 index c354d100d..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IContainer.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IContainer { - - // Methods - getSize(): number; - getSlot(slot: number): import('./item/IItemStack').IItemStack; - setSlot(slot: number, item: import('./item/IItemStack').IItemStack): void; - getMCInventory(): IInventory; - getMCContainer(): Container; - count(itemStack: import('./item/IItemStack').IItemStack, ignoreDamage: boolean, ignoreNBT: boolean): number; - getItems(): import('./item/IItemStack').IItemStack[]; - isCustomGUI(): boolean; - detectAndSendChanges(): void; - isPlayerNotUsingContainer(player: import('./entity/IPlayer').IPlayer): boolean; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IDamageSource.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IDamageSource.d.ts deleted file mode 100644 index 22db81c0b..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IDamageSource.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IDamageSource { - - // Methods - getType(): string; - isUnblockable(): boolean; - isProjectile(): boolean; - getTrueSource(): import('./entity/IEntity').IEntity; - getImmediateSource(): import('./entity/IEntity').IEntity; - getMCDamageSource(): DamageSource; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/INbt.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/INbt.d.ts deleted file mode 100644 index 0bb256fbc..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/INbt.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface INbt { - - // Methods - remove(key: string): void; - has(key: string): boolean; - getBoolean(key: string): boolean; - setBoolean(key: string, value: boolean): void; - getShort(key: string): short; - setShort(key: string, value: short): void; - getInteger(key: string): number; - setInteger(key: string, value: number): void; - getByte(key: string): byte; - setByte(key: string, value: byte): void; - getLong(key: string): number; - setLong(key: string, value: number): void; - getDouble(key: string): number; - setDouble(key: string, value: number): void; - getFloat(key: string): number; - setFloat(key: string, value: number): void; - getString(key: string): string; - setString(key: string, value: string): void; - getByteArray(key: string): byte[]; - setByteArray(key: string, value: byte[]): void; - getIntegerArray(key: string): number[]; - setIntegerArray(key: string, value: number[]): void; - getList(key: string, value: number): any[]; - getListType(key: string): number; - setList(key: string, value: any[]): void; - getCompound(key: string): import('./INbt').INbt; - setCompound(key: string, value: import('./INbt').INbt): void; - getKeys(): string[]; - getType(key: string): number; - getMCNBT(): NBTTagCompound; - toJsonString(): string; - isEqual(nbt: import('./INbt').INbt): boolean; - clear(): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IParticle.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IParticle.d.ts deleted file mode 100644 index a78fb94a3..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IParticle.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IParticle { - - // Methods - spawn(entity: import('./entity/IEntity').IEntity): void; - spawn(world: import('./IWorld').IWorld): void; - spawn(world: import('./IWorld').IWorld, x: number, y: number, z: number): void; - spawnOnEntity(entity: import('./entity/IEntity').IEntity): void; - spawnInWorld(world: import('./IWorld').IWorld): void; - spawnInWorld(world: import('./IWorld').IWorld, x: number, y: number, z: number): void; - setGlows(glows: boolean): void; - getGlows(): boolean; - setNoClip(noClip: boolean): void; - getNoClip(): boolean; - setFacePlayer(facePlayer: boolean): void; - getFacePlayer(): boolean; - setDirectory(directory: string): void; - getDirectory(): string; - setAmount(amount: number): void; - getAmount(): number; - setMaxAge(maxAge: number): void; - getMaxAge(): number; - setSize(width: number, height: number): void; - getWidth(): number; - getHeight(): number; - setOffset(offsetX: number, offsetY: number): void; - getOffsetX(): number; - getOffsetY(): number; - setAnim(animRate: number, animLoop: boolean, animStart: number, animEnd: number): void; - getAnimRate(): number; - getAnimLoop(): boolean; - getAnimStart(): number; - getAnimEnd(): number; - setPosition(x: number, y: number, z: number): void; - getX(): number; - getY(): number; - getZ(): number; - setPosition(pos: import('./IPos').IPos): void; - getPos(): void; - setMotion(motionX: number, motionY: number, motionZ: number, gravity: number): void; - getMotionX(): number; - getMotionY(): number; - getMotionZ(): number; - getGravity(): number; - setHEXColor(HEXColor: number, HEXColor2: number, HEXColorRate: number, HEXColorStart: number): void; - getHEXColor1(): number; - getHEXColor2(): number; - getHEXColorRate(): number; - getHEXColorStart(): number; - setAlpha(alpha1: number, alpha2: number, alphaRate: number, alphaRateStart: number): void; - getAlpha1(): number; - getAlpha2(): number; - getAlphaRate(): number; - getAlphaRateStart(): number; - setScale(scale1: number, scale2: number, scaleRate: number, scaleRateStart: number): void; - setScaleX(scale1: number, scale2: number, scaleRate: number, scaleRateStart: number): void; - getScaleX1(): number; - getScaleX2(): number; - getScaleXRate(): number; - getScaleXRateStart(): number; - setScaleY(scale1: number, scale2: number, scaleRate: number, scaleRateStart: number): void; - getScaleY1(): number; - getScaleY2(): number; - getScaleYRate(): number; - getScaleYRateStart(): number; - setRotationX(rotationX1: number, rotationX2: number, rotationXRate: number, rotationXRateStart: number): void; - getRotationX1(): number; - getRotationX2(): number; - getRotationXRate(): number; - getRotationXRateStart(): number; - setRotationY(rotationY1: number, rotationY2: number, rotationYRate: number, rotationYRateStart: number): void; - getRotationY1(): number; - getRotationY2(): number; - getRotationYRate(): number; - getRotationYRateStart(): number; - setRotationZ(rotationZ1: number, rotationZ2: number, rotationZRate: number, rotationZRateStart: number): void; - getRotationZ1(): number; - getRotationZ2(): number; - getRotationZRate(): number; - getRotationZRateStart(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPixelmonPlayerData.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPixelmonPlayerData.d.ts deleted file mode 100644 index a4cadcc80..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPixelmonPlayerData.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IPixelmonPlayerData { - - // Methods - getPartySlot(slot: number): import('./entity/IPixelmon').IPixelmon; - countPCPixelmon(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPos.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPos.d.ts deleted file mode 100644 index f6ac619ad..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IPos.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IPos { - - // Methods - getX(): number; - getY(): number; - getZ(): number; - getXD(): number; - getYD(): number; - getZD(): number; - up(): import('./IPos').IPos; - up(n: number): import('./IPos').IPos; - down(): import('./IPos').IPos; - down(n: number): import('./IPos').IPos; - north(): import('./IPos').IPos; - north(n: number): import('./IPos').IPos; - east(): import('./IPos').IPos; - east(n: number): import('./IPos').IPos; - south(): import('./IPos').IPos; - south(n: number): import('./IPos').IPos; - west(): import('./IPos').IPos; - west(n: number): import('./IPos').IPos; - add(x: number, y: number, z: number): import('./IPos').IPos; - add(pos: import('./IPos').IPos): import('./IPos').IPos; - subtract(x: number, y: number, z: number): import('./IPos').IPos; - subtract(pos: import('./IPos').IPos): import('./IPos').IPos; - normalize(): import('./IPos').IPos; - normalizeDouble(): number[]; - offset(direction: number): import('./IPos').IPos; - offset(direction: number, n: number): import('./IPos').IPos; - crossProduct(x: number, y: number, z: number): import('./IPos').IPos; - crossProduct(pos: import('./IPos').IPos): import('./IPos').IPos; - divide(scalar: number): import('./IPos').IPos; - toLong(): number; - fromLong(serialized: number): import('./IPos').IPos; - distanceTo(pos: import('./IPos').IPos): number; - distanceTo(x: number, y: number, z: number): number; - getMCPos(): import('../../../net/minecraft/util/math/BlockPos').BlockPos; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IScreenSize.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IScreenSize.d.ts deleted file mode 100644 index 69ecb96d1..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IScreenSize.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IScreenSize { - - // Methods - getWidth(): number; - getHeight(): number; - getWidthPercent(percent: number): number; - getHeightPercent(percent: number): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ISkinOverlay.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ISkinOverlay.d.ts deleted file mode 100644 index c0ebddeb6..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ISkinOverlay.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface ISkinOverlay { - - // Methods - setTexture(texture: string): void; - getTexture(): string; - setGlow(glow: boolean): void; - getGlow(): boolean; - setBlend(blend: boolean): void; - getBlend(): boolean; - setAlpha(alpha: number): void; - getAlpha(): number; - setSize(size: number): void; - getSize(): number; - setTextureScale(scaleX: number, scaleY: number): void; - getTextureScaleX(): number; - getTextureScaleY(): number; - setSpeed(speedX: number, speedY: number): void; - getSpeedX(): number; - getSpeedY(): number; - setOffset(offsetX: number, offsetY: number, offsetZ: number): void; - getOffsetX(): number; - getOffsetY(): number; - getOffsetZ(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITileEntity.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITileEntity.d.ts deleted file mode 100644 index 4371db88e..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITileEntity.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface ITileEntity { - - // Methods - getBlockMetadata(): number; - getWorld(): import('./IWorld').IWorld; - setWorld(world: import('./IWorld').IWorld): void; - getMCTileEntity(): TileEntity; - markDirty(): void; - readFromNBT(nbt: import('./INbt').INbt): void; - getDistanceFrom(x: number, y: number, z: number): number; - getDistanceFrom(pos: import('./IPos').IPos): number; - getBlockType(): import('./IBlock').IBlock; - isInvalid(): boolean; - invalidate(): void; - validate(): void; - updateContainingBlockInfo(): void; - getNBT(): import('./INbt').INbt; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITimers.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITimers.d.ts deleted file mode 100644 index 641c6de70..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/ITimers.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface ITimers { - - // Methods - timerIds(): number[]; - start(id: number, ticks: number, repeat: boolean): void; - forceStart(id: number, ticks: number, repeat: boolean): void; - has(id: number): boolean; - stop(id: number): boolean; - reset(id: number): void; - clear(): void; - ticks(id: number): number; - setTicks(id: number, ticks: number): void; - maxTicks(id: number): number; - setMaxTicks(id: number, maxTicks: number): void; - repeats(id: number): boolean; - setRepeats(id: number, repeat: boolean): void; - size(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IWorld.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IWorld.d.ts deleted file mode 100644 index 9c9e370c3..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/IWorld.d.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api - */ - -export interface IWorld { - - // Methods - getTime(): number; - getTotalTime(): number; - areAllPlayersAsleep(): boolean; - getBlock(x: number, y: number, z: number): import('./IBlock').IBlock; - getBlock(pos: import('./IPos').IPos): import('./IBlock').IBlock; - getTopBlock(x: number, z: number): import('./IBlock').IBlock; - getTopBlock(pos: import('./IPos').IPos): import('./IBlock').IBlock; - isBlockFreezable(pos: import('./IPos').IPos): boolean; - isBlockFreezable(x: number, y: number, z: number): boolean; - isBlockFreezableNaturally(pos: import('./IPos').IPos): boolean; - isBlockFreezableNaturally(x: number, y: number, z: number): boolean; - canBlockFreeze(pos: import('./IPos').IPos, adjacentToWater: boolean): boolean; - canBlockFreeze(x: number, y: number, z: number, adjacentToWater: boolean): boolean; - canBlockFreezeBody(pos: import('./IPos').IPos, adjacentToWater: boolean): boolean; - canBlockFreezeBody(x: number, y: number, z: number, adjacentToWater: boolean): boolean; - canSnowAt(pos: import('./IPos').IPos, checkLight: boolean): boolean; - canSnowAt(x: number, y: number, z: number, checkLight: boolean): boolean; - canSnowAtBody(pos: import('./IPos').IPos, checkLight: boolean): boolean; - canSnowAtBody(x: number, y: number, z: number, checkLight: boolean): boolean; - getHeightValue(x: number, z: number): number; - getHeightValue(pos: import('./IPos').IPos): number; - getChunkHeightMapMinimum(x: number, z: number): number; - getChunkHeightMapMinimum(pos: import('./IPos').IPos): number; - getBlockMetadata(x: number, y: number, z: number): number; - getBlockMetadata(pos: import('./IPos').IPos): number; - setBlockMetadataWithNotify(x: number, y: number, z: number, metadata: number, flag: number): boolean; - setBlockMetadataWithNotify(pos: import('./IPos').IPos, metadata: number, flag: number): boolean; - canSeeSky(x: number, y: number, z: number): boolean; - canSeeSky(pos: import('./IPos').IPos): boolean; - getFullBlockLightValue(x: number, y: number, z: number): number; - getFullBlockLightValue(pos: import('./IPos').IPos): number; - getBlockLightValue(x: number, y: number, z: number): number; - getBlockLightValue(pos: import('./IPos').IPos): number; - playSoundAtEntity(entity: import('./entity/IEntity').IEntity, sound: string, volume: number, pitch: number): void; - playSoundToNearExcept(player: import('./entity/IPlayer').IPlayer, sound: string, volume: number, pitch: number): void; - playSound(id: number, sound: import('./handler/data/ISound').ISound): void; - stopSound(id: number): void; - pauseSounds(): void; - continueSounds(): void; - stopSounds(): void; - getEntityByID(id: number): import('./entity/IEntity').IEntity; - spawnEntityInWorld(entity: import('./entity/IEntity').IEntity): boolean; - getClosestPlayerToEntity(entity: import('./entity/IEntity').IEntity, range: number): import('./entity/IPlayer').IPlayer; - getClosestPlayer(x: number, y: number, z: number, range: number): import('./entity/IPlayer').IPlayer; - getClosestPlayer(pos: import('./IPos').IPos, range: number): import('./entity/IPlayer').IPlayer; - getClosestVulnerablePlayerToEntity(entity: import('./entity/IEntity').IEntity, range: number): import('./entity/IPlayer').IPlayer; - getClosestVulnerablePlayer(x: number, y: number, z: number, range: number): import('./entity/IPlayer').IPlayer; - getClosestVulnerablePlayer(pos: import('./IPos').IPos, range: number): import('./entity/IPlayer').IPlayer; - countEntities(entity: import('./entity/IEntity').IEntity): number; - getLoadedEntities(): import('./entity/IEntity').IEntity[]; - getEntitiesNear(position: import('./IPos').IPos, range: number): import('./entity/IEntity').IEntity[]; - getEntitiesNear(x: number, y: number, z: number, range: number): import('./entity/IEntity').IEntity[]; - setTileEntity(x: number, y: number, z: number, tileEntity: import('./ITileEntity').ITileEntity): void; - setTileEntity(pos: import('./IPos').IPos, tileEntity: import('./ITileEntity').ITileEntity): void; - removeTileEntity(x: number, y: number, z: number): void; - removeTileEntity(pos: import('./IPos').IPos): void; - isBlockFullCube(x: number, y: number, z: number): boolean; - isBlockFullCube(pos: import('./IPos').IPos): boolean; - getSeed(): number; - setSpawnLocation(x: number, y: number, z: number): void; - setSpawnLocation(pos: import('./IPos').IPos): void; - canLightningStrikeAt(x: number, y: number, z: number): boolean; - canLightningStrikeAt(pos: import('./IPos').IPos): boolean; - isBlockHighHumidity(x: number, y: number, z: number): boolean; - isBlockHighHumidity(pos: import('./IPos').IPos): boolean; - getSignText(x: number, y: number, z: number): string; - getSignText(pos: import('./IPos').IPos): string; - setBlock(x: number, y: number, z: number, item: import('./item/IItemStack').IItemStack): boolean; - setBlock(pos: import('./IPos').IPos, item: import('./item/IItemStack').IItemStack): boolean; - setBlock(x: number, y: number, z: number, block: import('./IBlock').IBlock): boolean; - setBlock(pos: import('./IPos').IPos, block: import('./IBlock').IBlock): boolean; - removeBlock(x: number, y: number, z: number): void; - removeBlock(pos: import('./IPos').IPos): void; - isPlaceCancelled(posX: number, posY: number, posZ: number): boolean; - isPlaceCancelled(pos: import('./IPos').IPos): boolean; - isBreakCancelled(posX: number, posY: number, posZ: number): boolean; - isBreakCancelled(pos: import('./IPos').IPos): boolean; - rayCastPos(startPos: number[], lookVector: number[], maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IPos').IPos; - rayCastPos(startPos: number[], lookVector: number[], maxDistance: number): import('./IPos').IPos; - rayCastPos(startPos: import('./IPos').IPos, lookVector: import('./IPos').IPos, maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IPos').IPos; - rayCastPos(startPos: import('./IPos').IPos, lookVector: import('./IPos').IPos, maxDistance: number): import('./IPos').IPos; - rayCastBlock(startPos: number[], lookVector: number[], maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IBlock').IBlock; - rayCastBlock(startPos: number[], lookVector: number[], maxDistance: number): import('./IBlock').IBlock; - rayCastBlock(startPos: import('./IPos').IPos, lookVector: import('./IPos').IPos, maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IBlock').IBlock; - rayCastBlock(startPos: import('./IPos').IPos, lookVector: import('./IPos').IPos, maxDistance: number): import('./IBlock').IBlock; - getNearestAir(startPos: import('./IPos').IPos, maxHeight: number): import('./IPos').IPos; - rayCastEntities(startPos: number[], lookVector: number[], maxDistance: number, offset: number, range: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./entity/IEntity').IEntity[]; - rayCastEntities(ignoreEntities: import('./entity/IEntity').IEntity[], startPos: number[], lookVector: number[], maxDistance: number, offset: number, range: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./entity/IEntity').IEntity[]; - rayCastEntities(startPos: import('./IPos').IPos, lookVector: import('./IPos').IPos, maxDistance: number, offset: number, range: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./entity/IEntity').IEntity[]; - rayCastEntities(startPos: number[], lookVector: number[], maxDistance: number, offset: number, range: number): import('./entity/IEntity').IEntity[]; - rayCastEntities(startPos: import('./IPos').IPos, lookVector: import('./IPos').IPos, maxDistance: number, offset: number, range: number): import('./entity/IEntity').IEntity[]; - getPlayer(name: string): import('./entity/IPlayer').IPlayer; - getPlayerByUUID(uuid: string): import('./entity/IPlayer').IPlayer; - setTime(time: number): void; - isDay(): boolean; - isRaining(): boolean; - setRaining(bo: boolean): void; - thunderStrike(x: number, y: number, z: number): void; - thunderStrike(pos: import('./IPos').IPos): void; - spawnParticle(particle: string, x: number, y: number, z: number, dx: number, dy: number, dz: number, speed: number, count: number): void; - spawnParticle(particle: string, pos: import('./IPos').IPos, dx: number, dy: number, dz: number, speed: number, count: number): void; - createItem(id: string, damage: number, size: number): import('./item/IItemStack').IItemStack; - createEntityParticle(directory: string): import('./IParticle').IParticle; - getTempData(key: string): any; - setTempData(key: string, value: any): void; - hasTempData(key: string): boolean; - removeTempData(key: string): void; - clearTempData(): void; - getTempDataKeys(): string[]; - getStoredData(key: string): any; - setStoredData(key: string, value: any): void; - hasStoredData(key: string): boolean; - removeStoredData(key: string): void; - clearStoredData(): void; - getStoredDataKeys(): string[]; - explode(x: number, y: number, z: number, range: number, fire: boolean, grief: boolean): void; - explode(pos: import('./IPos').IPos, range: number, fire: boolean, grief: boolean): void; - getAllServerPlayers(): import('./entity/IPlayer').IPlayer[]; - getPlayerNames(): string[]; - getBiomeName(x: number, z: number): string; - getBiomeName(pos: import('./IPos').IPos): string; - spawnClone(x: number, y: number, z: number, tab: number, name: string, ignoreProtection: boolean): import('./entity/IEntity').IEntity; - spawnClone(pos: import('./IPos').IPos, tab: number, name: string, ignoreProtection: boolean): import('./entity/IEntity').IEntity; - spawnClone(x: number, y: number, z: number, tab: number, name: string): import('./entity/IEntity').IEntity; - spawnClone(pos: import('./IPos').IPos, tab: number, name: string): import('./entity/IEntity').IEntity; - getScoreboard(): import('./scoreboard/IScoreboard').IScoreboard; - getMCWorld(): WorldServer; - getDimensionID(): number; - broadcast(message: string): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/IBlockScripted.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/IBlockScripted.d.ts deleted file mode 100644 index bc39e8b69..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/IBlockScripted.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.block - */ - -export interface IBlockScripted extends IBlock { - - // Methods - setModel(item: import('../item/IItemStack').IItemStack): void; - setModel(name: string): void; - getModel(): import('../item/IItemStack').IItemStack; - getTimers(): import('../ITimers').ITimers; - setRedstonePower(strength: number): void; - getRedstonePower(): number; - setIsLadder(enabled: boolean): void; - getIsLadder(): boolean; - setLight(value: number): void; - getLight(): number; - setScale(x: number, y: number, z: number): void; - getScaleX(): number; - getScaleY(): number; - getScaleZ(): number; - setRotation(x: number, y: number, z: number): void; - getRotationX(): number; - getRotationY(): number; - getRotationZ(): number; - executeCommand(command: string): void; - getIsPassible(): boolean; - setIsPassible(bo: boolean): void; - getIsPassable(): boolean; - setIsPassable(bo: boolean): void; - getHardness(): number; - setHardness(hardness: number): void; - getResistance(): number; - setResistance(resistance: number): void; - getTextPlane(): import('./ITextPlane').ITextPlane; - getTextPlane2(): import('./ITextPlane').ITextPlane; - getTextPlane3(): import('./ITextPlane').ITextPlane; - getTextPlane4(): import('./ITextPlane').ITextPlane; - getTextPlane5(): import('./ITextPlane').ITextPlane; - getTextPlane6(): import('./ITextPlane').ITextPlane; - setStoredData(key: string, value: any): void; - getStoredData(key: string): any; - removeStoredData(key: string): void; - hasStoredData(key: string): boolean; - clearStoredData(): void; - getStoredDataKeys(): string[]; - removeTempData(key: string): void; - setTempData(key: string, value: any): void; - hasTempData(key: string): boolean; - getTempData(key: string): any; - clearTempData(): void; - getTempDataKeys(): string[]; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/ITextPlane.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/ITextPlane.d.ts deleted file mode 100644 index 57e44fba1..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/block/ITextPlane.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.block - */ - -export interface ITextPlane { - - // Methods - getText(): string; - setText(text: string): void; - getRotationX(): number; - getRotationY(): number; - getRotationZ(): number; - setRotationX(x: number): void; - setRotationY(y: number): void; - setRotationZ(z: number): void; - getOffsetX(): number; - getOffsetY(): number; - getOffsetZ(): number; - setOffsetX(x: number): void; - setOffsetY(y: number): void; - setOffsetZ(z: number): void; - getScale(): number; - setScale(scale: number): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimal.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimal.d.ts deleted file mode 100644 index 07681c954..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimal.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IAnimal extends IEntityLiving { - - // Methods - isBreedingItem(itemStack: import('../item/IItemStack').IItemStack): boolean; - interact(player: import('./IPlayer').IPlayer): boolean; - setFollowPlayer(player: import('./IPlayer').IPlayer): void; - followingPlayer(): import('./IPlayer').IPlayer; - isInLove(): boolean; - resetInLove(): void; - canMateWith(animal: import('./IAnimal').IAnimal): boolean; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts deleted file mode 100644 index bd933971b..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IAnimatable.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IAnimatable { - - // Methods - getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts deleted file mode 100644 index 719076b1a..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IArrow.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IArrow extends IEntity { - - // Methods - getShooter(): import('./IEntity').IEntity; - getDamage(): number; - kill(): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts deleted file mode 100644 index d99948448..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/ICustomNpc.d.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface ICustomNpc extends IEntityLiving, IAnimatable { - - // Methods - getSize(): number; - setSize(size: number): void; - getModelType(): number; - setModelType(modelType: number): void; - getName(): string; - setRotation(rotation: number): void; - setRotationType(rotationType: number): void; - getRotationType(): number; - setMovingType(movingType: number): void; - getMovingType(): number; - setName(name: string): void; - getTitle(): string; - setTitle(title: string): void; - getTexture(): string; - setTexture(texture: string): void; - getHome(): import('../IPos').IPos; - getHomeX(): number; - setHomeX(x: number): void; - getHomeY(): number; - setHomeY(y: number): void; - getHomeZ(): number; - setHomeZ(z: number): void; - setHome(x: number, y: number, z: number): void; - setHome(pos: import('../IPos').IPos): void; - setMaxHealth(health: number): void; - setReturnToHome(bo: boolean): void; - getReturnToHome(): boolean; - getFaction(): import('../handler/data/IFaction').IFaction; - setFaction(id: number): void; - setAttackFactions(attackOtherFactions: boolean): void; - getAttackFactions(): boolean; - setDefendFaction(defendFaction: boolean): void; - getDefendFaction(): boolean; - getType(): number; - typeOf(type: number): boolean; - shootItem(target: IEntityLivingBase, item: import('../item/IItemStack').IItemStack, accuracy: number): void; - setProjectilesKeepTerrain(b: boolean): void; - getProjectilesKeepTerrain(): boolean; - say(message: string): void; - say(player: import('./IPlayer').IPlayer, message: string): void; - getDialog(slot: number): import('../handler/data/IDialog').IDialog; - getDialogId(slot: number): number; - setDialog(slot: number, dialog: import('../handler/data/IDialog').IDialog): void; - setDialog(slot: number, dialogId: number): void; - getInteractLines(): import('../handler/data/ILines').ILines; - getWorldLines(): import('../handler/data/ILines').ILines; - getAttackLines(): import('../handler/data/ILines').ILines; - getKilledLines(): import('../handler/data/ILines').ILines; - getKillLines(): import('../handler/data/ILines').ILines; - kill(): void; - reset(): void; - getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; - getRole(): import('../roles/IRole').IRole; - setRole(role: number): void; - getJob(): import('../jobs/IJob').IJob; - setJob(job: number): void; - getRightItem(): import('../item/IItemStack').IItemStack; - setRightItem(item: import('../item/IItemStack').IItemStack): void; - getLefttItem(): import('../item/IItemStack').IItemStack; - getLeftItem(): import('../item/IItemStack').IItemStack; - setLeftItem(item: import('../item/IItemStack').IItemStack): void; - getProjectileItem(): import('../item/IItemStack').IItemStack; - setProjectileItem(item: import('../item/IItemStack').IItemStack): void; - canAimWhileShooting(): boolean; - aimWhileShooting(aimWhileShooting: boolean): void; - setAimType(aimWhileShooting: byte): void; - getAimType(): byte; - setMinProjectileDelay(minDelay: number): void; - getMinProjectileDelay(): number; - setMaxProjectileDelay(maxDelay: number): void; - getMaxProjectileDelay(): number; - setRangedRange(rangedRange: number): void; - getRangedRange(): number; - setFireRate(rate: number): void; - getFireRate(): number; - setBurstCount(burstCount: number): void; - getBurstCount(): number; - setShotCount(shotCount: number): void; - getShotCount(): number; - setAccuracy(accuracy: number): void; - getAccuracy(): number; - getFireSound(): string; - setFireSound(fireSound: string): void; - getArmor(slot: number): import('../item/IItemStack').IItemStack; - setArmor(slot: number, item: import('../item/IItemStack').IItemStack): void; - getLootItem(slot: number): import('../item/IItemStack').IItemStack; - setLootItem(slot: number, item: import('../item/IItemStack').IItemStack): void; - getLootChance(slot: number): number; - setLootChance(slot: number, chance: number): void; - getLootMode(): number; - setLootMode(lootMode: number): void; - setMinLootXP(lootXP: number): void; - setMaxLootXP(lootXP: number): void; - getMinLootXP(): number; - getMaxLootXP(): number; - getCanDrown(): boolean; - setDrowningType(type: number): void; - canBreathe(): boolean; - setAnimation(type: number): void; - setTacticalVariant(variant: number): void; - getTacticalVariant(): number; - setTacticalVariant(variant: string): void; - getTacticalVariantName(): string; - getCombatPolicyName(): string; - setCombatPolicy(policy: number): void; - getCombatPolicy(): number; - setCombatPolicy(policy: string): void; - setTacticalRadius(tacticalRadius: number): void; - getTacticalRadius(): number; - setIgnoreCobweb(ignore: boolean): void; - getIgnoreCobweb(): boolean; - setOnFoundEnemy(onAttack: number): void; - onFoundEnemy(): number; - setShelterFrom(shelterFrom: number): void; - getShelterFrom(): number; - hasLivingAnimation(): boolean; - setLivingAnimation(livingAnimation: boolean): void; - setVisibleType(type: number): void; - getVisibleType(): number; - setVisibleTo(player: import('./IPlayer').IPlayer, visible: boolean): void; - isVisibleTo(player: import('./IPlayer').IPlayer): boolean; - setShowName(type: number): void; - getShowName(): number; - getShowBossBar(): number; - setShowBossBar(type: number): void; - getMeleeStrength(): number; - setMeleeStrength(strength: number): void; - getMeleeSpeed(): number; - setMeleeSpeed(speed: number): void; - getMeleeRange(): number; - setMeleeRange(range: number): void; - getSwingWarmup(): number; - setSwingWarmup(ticks: number): void; - getKnockback(): number; - setKnockback(knockback: number): void; - getAggroRange(): number; - setAggroRange(aggroRange: number): void; - getRangedStrength(): number; - setRangedStrength(strength: number): void; - getRangedSpeed(): number; - setRangedSpeed(speed: number): void; - getRangedBurst(): number; - setRangedBurst(count: number): void; - getRespawnTime(): number; - setRespawnTime(time: number): void; - getRespawnCycle(): number; - setRespawnCycle(cycle: number): void; - getHideKilledBody(): boolean; - hideKilledBody(hide: boolean): void; - naturallyDespawns(): boolean; - setNaturallyDespawns(canDespawn: boolean): void; - spawnedFromSoulStone(): boolean; - getSoulStonePlayerName(): string; - isSoulStoneInit(): boolean; - getRefuseSoulStone(): boolean; - setRefuseSoulStone(refuse: boolean): void; - getMinPointsToSoulStone(): number; - setMinPointsToSoulStone(points: number): void; - giveItem(player: import('./IPlayer').IPlayer, item: import('../item/IItemStack').IItemStack): void; - executeCommand(command: string): void; - getModelData(): import('./data/IModelData').IModelData; - setHeadScale(x: number, y: number, z: number): void; - setBodyScale(x: number, y: number, z: number): void; - setArmsScale(x: number, y: number, z: number): void; - setLegsScale(x: number, y: number, z: number): void; - setExplosionResistance(resistance: number): void; - getExplosionResistance(): number; - setMeleeResistance(resistance: number): void; - getMeleeResistance(): number; - setArrowResistance(resistance: number): void; - getArrowResistance(): number; - setKnockbackResistance(resistance: number): void; - getKnockbackResistance(): number; - setRetaliateType(type: number): void; - getCombatRegen(): number; - setCombatRegen(regen: number): void; - getHealthRegen(): number; - setHealthRegen(regen: number): void; - getAge(): number; - getTimers(): import('../ITimers').ITimers; - setFly(fly: number): void; - canFly(): boolean; - setFlySpeed(flySpeed: number): void; - getFlySpeed(unused: number): number; - setFlyGravity(flyGravity: number): void; - getFlyGravity(unused: number): number; - setFlyHeightLimit(flyHeightLimit: number): void; - getFlyHeightLimit(unused: number): number; - limitFlyHeight(limit: boolean): void; - isFlyHeightLimited(unused: boolean): boolean; - setSpeed(speed: number): void; - getSpeed(): number; - setSkinType(type: byte): void; - getSkinType(): byte; - setSkinUrl(url: string): void; - getSkinUrl(): string; - setCloakTexture(cloakTexture: string): void; - getCloakTexture(): string; - setOverlayTexture(overlayTexture: string): void; - getOverlayTexture(): string; - getOverlays(): import('../handler/IOverlayHandler').IOverlayHandler; - setCollisionType(type: number): void; - getCollisionType(): number; - updateClient(): void; - updateAI(): void; - getActionManager(): import('../handler/IActionManager').IActionManager; - getMagicData(): import('../handler/data/IMagicData').IMagicData; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts deleted file mode 100644 index 0d3fa22c9..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IDBCPlayer.d.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IDBCPlayer extends IPlayer { - - // Methods - setStat(stat: string, value: number): void; - getStat(stat: string): number; - addBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number): void; - addBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number, endOfTheList: boolean): void; - addToBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number): void; - setBonusAttribute(stat: string, bonusID: string, operation: string, attributeValue: number): void; - getBonusAttribute(stat: string, bonusID: string): void; - removeBonusAttribute(stat: string, bonusID: string): void; - clearBonusAttribute(stat: string): void; - bonusAttribute(action: string, stat: string, bonusID: string): string; - bonusAttribute(action: string, stat: string, bonusID: string, operation: string, attributeValue: number, endOfTheList: boolean): string; - setRelease(release: byte): void; - getRelease(): byte; - setBody(body: number): void; - getBody(): number; - setHP(hp: number): void; - getHP(): number; - setStamina(stamina: number): void; - getStamina(): number; - setKi(ki: number): void; - getKi(): number; - setTP(tp: number): void; - getTP(): number; - setGravity(gravity: number): void; - getGravity(): number; - isBlocking(): boolean; - setHairCode(hairCode: string): void; - getHairCode(): string; - setExtraCode(extraCode: string): void; - getExtraCode(): string; - setItem(itemStack: import('../item/IItemStack').IItemStack, slot: byte, vanity: boolean): void; - getItem(slot: byte, vanity: boolean): import('../item/IItemStack').IItemStack; - getInventory(): import('../item/IItemStack').IItemStack[]; - setForm(form: byte): void; - getForm(): byte; - setForm2(form2: byte): void; - getForm2(): byte; - getRacialFormMastery(form: byte): number; - setRacialFormMastery(form: byte, value: number): void; - addRacialFormMastery(form: byte, value: number): void; - getOtherFormMastery(formName: string): number; - setOtherFormMastery(formName: string, value: number): void; - addOtherFormMastery(formName: string, value: number): void; - setPowerPoints(points: number): void; - getPowerPoints(): number; - setAuraColor(color: number): void; - getAuraColor(): number; - setFormLevel(level: number): void; - getFormLevel(): number; - setSkills(skills: string): void; - getSkills(): string; - setJRMCSE(statusEffects: string): void; - getJRMCSE(): string; - setRace(race: byte): void; - getRace(): number; - setDBCClass(dbcClass: byte): void; - getDBCClass(): byte; - setPowerType(powerType: byte): void; - getPowerType(): number; - getKillCount(type: string): number; - getFusionString(): string; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts deleted file mode 100644 index 49f0fd439..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntity.d.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IEntity { - - // Methods - spawnParticle(entityParticle: import('../IParticle').IParticle): void; - getEntityId(): number; - getUniqueID(): string; - getYOffset(): number; - getWidth(): number; - getHeight(): number; - getX(): number; - setX(x: number): void; - getY(): number; - setY(y: number): void; - getZ(): number; - setZ(z: number): void; - getMotionX(): number; - setMotionX(x: number): void; - getMotionY(): number; - setMotionY(y: number): void; - getMotionZ(): number; - setMotionZ(z: number): void; - setMotion(x: number, y: number, z: number): void; - setMotion(pos: import('../IPos').IPos): void; - getMotion(): import('../IPos').IPos; - isAirborne(): boolean; - getBlockX(): number; - getBlockY(): number; - getBlockZ(): number; - setPosition(x: number, y: number, z: number): void; - setPosition(pos: import('../IPos').IPos): void; - getPosition(): import('../IPos').IPos; - getDimension(): number; - setDimension(dimensionId: number): void; - getCollidingEntities(): import('./IEntity').IEntity[]; - getSurroundingEntities(range: number): import('./IEntity').IEntity[]; - getSurroundingEntities(range: number, type: number): import('./IEntity').IEntity[]; - isAlive(): boolean; - getTempData(key: string): any; - setTempData(key: string, value: any): void; - hasTempData(key: string): boolean; - removeTempData(key: string): void; - clearTempData(): void; - getTempDataKeys(): string[]; - getStoredData(key: string): any; - setStoredData(key: string, value: any): void; - hasStoredData(key: string): boolean; - removeStoredData(key: string): void; - clearStoredData(): void; - getStoredDataKeys(): string[]; - getAge(): number; - despawn(): void; - inWater(): boolean; - inLava(): boolean; - inFire(): boolean; - isBurning(): boolean; - setBurning(ticks: number): void; - extinguish(): void; - getTypeName(): string; - dropItem(item: import('../item/IItemStack').IItemStack): void; - getRider(): import('./IEntity').IEntity; - setRider(entity: import('./IEntity').IEntity): void; - getMount(): import('./IEntity').IEntity; - setMount(entity: import('./IEntity').IEntity): void; - getType(): number; - typeOf(type: number): boolean; - setRotation(rotation: number): void; - setRotation(rotationYaw: number, rotationPitch: number): void; - getRotation(): number; - setPitch(pitch: number): void; - getPitch(): number; - knockback(power: number, direction: number): void; - knockback(xpower: number, ypower: number, zpower: number, direction: number): void; - knockback(pos: import('../IPos').IPos, direction: number): void; - setImmune(ticks: number): void; - setInvisible(invisible: boolean): void; - setSneaking(sneaking: boolean): void; - setSprinting(sprinting: boolean): void; - hasCollided(): boolean; - hasCollidedVertically(): boolean; - hasCollidedHorizontally(): boolean; - capturesDrops(): boolean; - setCapturesDrops(capture: boolean): void; - setCapturedDrops(capturedDrops: IEntity { - - // Methods - getOwner(): string; - setOwner(name: string): void; - getThrower(): string; - setThrower(name: string): void; - getPickupDelay(): number; - setPickupDelay(delay: number): void; - getAge(): number; - setAge(age: number): void; - getLifeSpawn(): number; - setLifeSpawn(age: number): void; - getItem(): import('../item/IItemStack').IItemStack; - setItem(item: import('../item/IItemStack').IItemStack): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts deleted file mode 100644 index fbb890558..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLiving.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IEntityLiving extends IEntityLivingBase { - - // Methods - isNavigating(): boolean; - clearNavigation(): void; - navigateTo(x: number, y: number, z: number, speed: number): void; - getMCEntity(): T; - playLivingSound(): void; - spawnExplosionParticle(): void; - setMoveForward(speed: number): void; - faceEntity(entity: import('./IEntity').IEntity, pitch: number, yaw: number): void; - canPickUpLoot(): boolean; - setCanPickUpLoot(pickUp: boolean): void; - isPersistent(): boolean; - enablePersistence(): void; - setCustomNameTag(text: string): void; - getCustomNameTag(): string; - hasCustomNameTag(): boolean; - setAlwaysRenderNameTag(alwaysRender: boolean): void; - getAlwaysRenderNameTag(): boolean; - clearLeashed(sendPacket: boolean, dropLeash: boolean): void; - allowLeashing(): boolean; - getLeashed(): boolean; - getLeashedTo(): import('./IEntity').IEntity; - setLeashedTo(entity: import('./IEntity').IEntity, sendPacket: boolean): void; - canBeSteered(): boolean; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts deleted file mode 100644 index 3a9aef6ce..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IEntityLivingBase.d.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IEntityLivingBase extends import('./IEntity').IEntity { - - // Methods - getHealth(): number; - setHealth(health: number): void; - hurt(damage: number): void; - hurt(damage: number, source: import('./IEntity').IEntity): void; - hurt(damage: number, damageSource: import('../IDamageSource').IDamageSource): void; - setMaxHurtTime(time: number): void; - getMaxHurtTime(): number; - getMaxHealth(): number; - getFollowRange(): number; - getKnockbackResistance(): number; - getSpeed(): number; - getMeleeStrength(): number; - setMaxHealth(health: number): void; - setFollowRange(range: number): void; - setKnockbackResistance(knockbackResistance: number): void; - setSpeed(speed: number): void; - setMeleeStrength(attackDamage: number): void; - isAttacking(): boolean; - setAttackTarget(living: IEntityLivingBase): void; - getAttackTarget(): IEntityLivingBase; - getAttackTargetTime(): number; - setLastAttacker(p_130011_1_: import('./IEntity').IEntity): void; - getLastAttacker(): import('./IEntity').IEntity; - getLastAttackerTime(): number; - canBreatheUnderwater(): boolean; - getType(): number; - typeOf(type: number): boolean; - getLookVector(): import('../IPos').IPos; - getLookingAtBlock(maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('../IBlock').IBlock; - getLookingAtBlock(maxDistance: number): import('../IBlock').IBlock; - getLookingAtPos(maxDistance: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('../IPos').IPos; - getLookingAtPos(maxDistance: number): import('../IPos').IPos; - getLookingAtEntities(ignoreEntities: import('./IEntity').IEntity[], maxDistance: number, offset: number, range: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IEntity').IEntity[]; - getLookingAtEntities(maxDistance: number, offset: number, range: number, stopOnBlock: boolean, stopOnLiquid: boolean, stopOnCollision: boolean): import('./IEntity').IEntity[]; - getLookingAtEntities(maxDistance: number, offset: number, range: number): import('./IEntity').IEntity[]; - getMCEntity(): T; - swingHand(): void; - addPotionEffect(effect: number, duration: number, strength: number, hideParticles: boolean): void; - clearPotionEffects(): void; - getPotionEffect(effect: number): number; - getHeldItem(): import('../item/IItemStack').IItemStack; - setHeldItem(item: import('../item/IItemStack').IItemStack): void; - getArmor(slot: number): import('../item/IItemStack').IItemStack; - setArmor(slot: number, item: import('../item/IItemStack').IItemStack): void; - isChild(): boolean; - renderBrokenItemStack(itemStack: import('../item/IItemStack').IItemStack): void; - isOnLadder(): boolean; - getTotalArmorValue(): number; - getArrowCountInEntity(): number; - setArrowCountInEntity(count: number): void; - dismountEntity(entity: import('./IEntity').IEntity): void; - setAIMoveSpeed(speed: number): void; - getAIMoveSpeed(): number; - setAbsorptionAmount(amount: number): void; - getAbsorptionAmount(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts deleted file mode 100644 index 4b1c9b09f..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IFishHook.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IFishHook extends IEntity { - - // Methods - getCaster(): import('./IPlayer').IPlayer; - kill(): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts deleted file mode 100644 index 10c4632d1..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IMonster.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IMonster extends IEntityLiving { - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts deleted file mode 100644 index b397fb06c..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPixelmon.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IPixelmon extends IAnimal { - - // Methods - getIsShiny(): boolean; - setIsShiny(bo: boolean): void; - getLevel(): number; - setLevel(level: number): void; - getIV(type: number): number; - setIV(type: number, value: number): void; - getEV(type: number): number; - setEV(type: number, value: number): void; - getStat(type: number): number; - setStat(type: number, value: number): void; - getSize(): number; - setSize(type: number): void; - getHapiness(): number; - setHapiness(value: number): void; - getNature(): number; - setNature(type: number): void; - getPokeball(): number; - setPokeball(type: number): void; - getNickname(): string; - hasNickname(): boolean; - setNickname(name: string): void; - getMove(slot: number): string; - setMove(slot: number, move: string): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts deleted file mode 100644 index efeaafc75..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IPlayer.d.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IPlayer extends IEntityLivingBase, IAnimatable { - - // Methods - getDisplayName(): string; - getName(): string; - kick(reason: string): void; - setPosition(x: number, y: number, z: number): void; - setPosition(pos: import('../IPos').IPos): void; - setPosition(x: number, y: number, z: number, dimensionId: number): void; - setPosition(pos: import('../IPos').IPos, dimensionId: number): void; - setPosition(x: number, y: number, z: number, world: import('../IWorld').IWorld): void; - setPosition(pos: import('../IPos').IPos, world: import('../IWorld').IWorld): void; - setDimension(dimension: number): void; - getHunger(): number; - setHunger(hunger: number): void; - getSaturation(): number; - setSaturation(saturation: number): void; - showDialog(dialog: import('../handler/data/IDialog').IDialog): void; - hasReadDialog(dialog: import('../handler/data/IDialog').IDialog): boolean; - readDialog(dialog: import('../handler/data/IDialog').IDialog): void; - unreadDialog(dialog: import('../handler/data/IDialog').IDialog): void; - showDialog(id: number): void; - hasReadDialog(id: number): boolean; - readDialog(id: number): void; - unreadDialog(id: number): void; - hasFinishedQuest(quest: import('../handler/data/IQuest').IQuest): boolean; - hasActiveQuest(quest: import('../handler/data/IQuest').IQuest): boolean; - startQuest(quest: import('../handler/data/IQuest').IQuest): void; - finishQuest(quest: import('../handler/data/IQuest').IQuest): void; - stopQuest(quest: import('../handler/data/IQuest').IQuest): void; - removeQuest(quest: import('../handler/data/IQuest').IQuest): void; - hasFinishedQuest(id: number): boolean; - hasActiveQuest(id: number): boolean; - startQuest(id: number): void; - finishQuest(id: number): void; - stopQuest(id: number): void; - removeQuest(id: number): void; - getFinishedQuests(): import('../handler/data/IQuest').IQuest[]; - getType(): number; - typeOf(type: number): boolean; - addFactionPoints(faction: number, points: number): void; - setFactionPoints(faction: number, points: number): void; - getFactionPoints(faction: number): number; - sendMessage(message: string): void; - getMode(): number; - setMode(type: number): void; - getInventory(): import('../item/IItemStack').IItemStack[]; - inventoryItemCount(item: import('../item/IItemStack').IItemStack, ignoreNBT: boolean, ignoreDamage: boolean): number; - removeItem(id: string, damage: number, amount: number): boolean; - removeItem(item: import('../item/IItemStack').IItemStack, amount: number, ignoreNBT: boolean, ignoreDamage: boolean): boolean; - removeAllItems(item: import('../item/IItemStack').IItemStack, ignoreNBT: boolean, ignoreDamage: boolean): number; - giveItem(item: import('../item/IItemStack').IItemStack, amount: number): boolean; - giveItem(id: string, damage: number, amount: number): boolean; - setSpawnpoint(x: number, y: number, z: number): void; - setSpawnpoint(pos: import('../IPos').IPos): void; - resetSpawnpoint(): void; - setRotation(rotationYaw: number, rotationPitch: number): void; - disableMouseInput(time: number, buttonIds: ): void; - stopUsingItem(): void; - clearItemInUse(): void; - clearInventory(): void; - playSound(name: string, volume: number, pitch: number): void; - playSound(id: number, sound: import('../handler/data/ISound').ISound): void; - playSound(sound: import('../handler/data/ISound').ISound): void; - stopSound(id: number): void; - pauseSounds(): void; - continueSounds(): void; - stopSounds(): void; - mountEntity(ridingEntity: Entity): void; - dropOneItem(dropStack: boolean): import('./IEntity').IEntity; - canHarvestBlock(block: import('../IBlock').IBlock): boolean; - interactWith(entity: import('./IEntity').IEntity): boolean; - hasAchievement(achievement: string): boolean; - hasBukkitPermission(permission: string): boolean; - getExpLevel(): number; - setExpLevel(level: number): void; - getPixelmonData(): import('../IPixelmonPlayerData').IPixelmonPlayerData; - getTimers(): import('../ITimers').ITimers; - updatePlayerInventory(): void; - getDBCPlayer(): import('../../kamkeel/npcdbc/api/IDBCAddon').IDBCAddon; - blocking(): boolean; - getData(): import('../handler/IPlayerData').IPlayerData; - isScriptingDev(): boolean; - getActiveQuests(): import('../handler/data/IQuest').IQuest[]; - getOpenContainer(): import('../IContainer').IContainer; - showCustomGui(gui: import('../gui/ICustomGui').ICustomGui): void; - getCustomGui(): import('../gui/ICustomGui').ICustomGui; - closeGui(): void; - showCustomOverlay(overlay: import('../overlay/ICustomOverlay').ICustomOverlay): void; - closeOverlay(id: number): void; - getOverlays(): import('../handler/IOverlayHandler').IOverlayHandler; - getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; - setConqueredEnd(conqueredEnd: boolean): void; - conqueredEnd(): boolean; - getScreenSize(): import('../IScreenSize').IScreenSize; - getMagicData(): import('../handler/data/IMagicData').IMagicData; - getAttributes(): import('../handler/data/IPlayerAttributes').IPlayerAttributes; - getActionManager(): import('../handler/IActionManager').IActionManager; - getPartyMembers(): import('./IPlayer').IPlayer[]; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts deleted file mode 100644 index d3142f27e..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IProjectile.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IProjectile { - - // Methods - getItem(): import('../item/IItemStack').IItemStack; - setItem(item: import('../item/IItemStack').IItemStack): void; - getHasGravity(): boolean; - setHasGravity(bo: boolean): void; - getAccuracy(): number; - setAccuracy(accuracy: number): void; - setHeading(entity: import('./IEntity').IEntity): void; - setHeading(x: number, y: number, z: number): void; - setHeading(yaw: number, pitch: number): void; - getThrower(): import('./IEntity').IEntity; - enableEvents(): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts deleted file mode 100644 index 3eb2842f9..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IThrowable.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IThrowable extends IEntity { - - // Methods - getThrower(): IEntityLivingBase; - kill(): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts deleted file mode 100644 index c9a444f4d..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/IVillager.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity - */ - -export interface IVillager extends IEntityLiving { - - // Methods - getProfession(): number; - getIsTrading(): boolean; - getCustomer(): IEntityLivingBase; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts deleted file mode 100644 index d7b466ec2..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IMark.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity.data - */ - -export interface IMark { - - // Methods - getAvailability(): import('../../handler/data/IAvailability').IAvailability; - getColor(): number; - setColor(color: number): void; - getType(): number; - setType(type: number): void; - update(): void; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts deleted file mode 100644 index 78239eb82..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelData.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity.data - */ - -export interface IModelData { - - // Methods - headWear(config: byte): void; - headWear(): byte; - bodyWear(config: byte): void; - bodyWear(): byte; - rightArmWear(config: byte): void; - rightArmWear(): byte; - leftArmWear(config: byte): void; - leftArmWear(): byte; - rightLegWear(config: byte): void; - rightLegWear(): byte; - leftLegWear(config: byte): void; - leftLegWear(): byte; - hidePart(part: number, hide: byte): void; - hidden(part: number): number; - enableRotation(enableRotation: boolean): void; - enableRotation(): boolean; - getRotation(): import('./IModelRotate').IModelRotate; - getScale(): import('./IModelScale').IModelScale; - setEntity(string: string): void; - getEntity(): string; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts deleted file mode 100644 index c873533a7..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotate.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity.data - */ - -export interface IModelRotate { - - // Methods - whileStanding(): boolean; - whileStanding(whileStanding: boolean): void; - whileAttacking(): boolean; - whileAttacking(whileAttacking: boolean): void; - whileMoving(): boolean; - whileMoving(whileMoving: boolean): void; - getPart(part: number): import('./IModelRotatePart').IModelRotatePart; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts deleted file mode 100644 index 58df0518b..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelRotatePart.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity.data - */ - -export interface IModelRotatePart { - - // Methods - setRotation(x: number, y: number, z: number): void; - getRotateX(): number; - getRotateY(): number; - getRotateZ(): number; - disabled(enabled: boolean): void; - disabled(): boolean; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts deleted file mode 100644 index a47b29f59..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScale.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity.data - */ - -export interface IModelScale { - - // Methods - getPart(part: number): import('./IModelScalePart').IModelScalePart; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts deleted file mode 100644 index d0c089eb8..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/entity/data/IModelScalePart.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.entity.data - */ - -export interface IModelScalePart { - - // Methods - setScale(x: number, y: number, z: number): void; - getScaleX(): number; - getScaleY(): number; - getScaleZ(): number; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts deleted file mode 100644 index 906f120e5..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IAnimationEvent.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IAnimationEvent extends ICustomNPCsEvent { - - // Methods - getAnimation(): import('../handler/data/IAnimation').IAnimation; - getAnimationData(): import('../handler/data/IAnimationData').IAnimationData; - getEntity(): import('../entity/IAnimatable').IAnimatable; - - // Nested interfaces - interface Started extends IAnimationEvent { - } - interface Ended extends IAnimationEvent { - } - interface IFrameEvent extends IAnimationEvent { - getIndex(): number; - getFrame(): import('../handler/data/IFrame').IFrame; - } - interface Entered extends IFrameEvent { - } - interface Exited extends IFrameEvent { - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts deleted file mode 100644 index 38f60b47d..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IBlockEvent.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IBlockEvent extends ICustomNPCsEvent { - getBlock(): import('../IBlock').IBlock; -} - -export namespace IBlockEvent { - export interface EntityFallenUponEvent extends IBlockEvent { - getEntity(): import('../entity/IEntity').IEntity; - getDistanceFallen(): number; - } - export interface InteractEvent extends IBlockEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getHitX(): number; - getHitY(): number; - getHitZ(): number; - getSide(): number; - } - export interface RedstoneEvent extends IBlockEvent { - getPrevPower(): number; - getPower(): number; - } - export interface BreakEvent extends IBlockEvent { - } - export interface ExplodedEvent extends IBlockEvent { - } - export interface RainFillEvent extends IBlockEvent { - } - export interface NeighborChangedEvent extends IBlockEvent { - getChangedPos(): import('../IPos').IPos; - } - export interface InitEvent extends IBlockEvent { - } - export interface UpdateEvent extends IBlockEvent { - } - export interface ClickedEvent extends IBlockEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface HarvestedEvent extends IBlockEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface CollidedEvent extends IBlockEvent { - getEntity(): import('../entity/IEntity').IEntity; - } - export interface TimerEvent extends IBlockEvent { - getId(): number; - } -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts deleted file mode 100644 index ae87a3319..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomGuiEvent.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface ICustomGuiEvent extends IPlayerEvent { - - // Methods - getGui(): import('../gui/ICustomGui').ICustomGui; - getId(): number; - - // Nested interfaces - interface ButtonEvent extends ICustomGuiEvent { - } - interface UnfocusedEvent extends ICustomGuiEvent { - } - interface CloseEvent extends ICustomGuiEvent { - } - interface ScrollEvent extends ICustomGuiEvent { - getSelection(): string[]; - doubleClick(): boolean; - getScrollIndex(): number; - } - interface SlotEvent extends ICustomGuiEvent { - getStack(): import('../item/IItemStack').IItemStack; - } - interface SlotClickEvent extends ICustomGuiEvent { - getStack(): import('../item/IItemStack').IItemStack; - getDragType(): number; - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts deleted file mode 100644 index d09f119fb..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ICustomNPCsEvent.d.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface ICustomNPCsEvent { - getHookName(): string; -} - -export namespace ICustomNPCsEvent { - export interface CNPCNaturalSpawnEvent extends ICustomNPCsEvent { - getNaturalSpawn(): import('../handler/data/INaturalSpawn').INaturalSpawn; - setAttemptPosition(attemptPosition: import('../IPos').IPos): void; - getAttemptPosition(): import('../IPos').IPos; - animalSpawnPassed(): boolean; - monsterSpawnPassed(): boolean; - liquidSpawnPassed(): boolean; - airSpawnPassed(): boolean; - } - export interface ScriptedCommandEvent extends ICustomNPCsEvent { - getSenderWorld(): import('../IWorld').IWorld; - getSenderPosition(): import('../IPos').IPos; - getSenderName(): string; - setReplyMessage(message: string): void; - getId(): string; - getArgs(): string[]; - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts deleted file mode 100644 index ad4624e5d..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IDialogEvent.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IDialogEvent extends IPlayerEvent { - - // Methods - getDialog(): import('../handler/data/IDialog').IDialog; - - // Nested interfaces - interface DialogOpen extends IDialogEvent { - } - interface DialogOption extends IDialogEvent { - } - interface DialogClosed extends IDialogEvent { - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts deleted file mode 100644 index f2e62ec13..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IFactionEvent.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IFactionEvent extends IPlayerEvent { - - // Methods - getFaction(): import('../handler/data/IFaction').IFaction; - - // Nested interfaces - interface FactionPoints extends IFactionEvent { - decreased(): boolean; - getPoints(): number; - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts deleted file mode 100644 index 454e3963c..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IForgeEvent.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IForgeEvent extends ICustomNPCsEvent { - getEvent(): any; -} - -export namespace IForgeEvent { - export interface WorldEvent extends IForgeEvent { - getWorld(): import('../IWorld').IWorld; - } - export interface EntityEvent extends IForgeEvent { - getEntity(): import('../entity/IEntity').IEntity; - } - export interface InitEvent extends IForgeEvent { - } -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts deleted file mode 100644 index 58a1cd805..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IItemEvent.d.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IItemEvent extends ICustomNPCsEvent { - getItem(): import('../item/IItemCustomizable').IItemCustomizable; -} - -export namespace IItemEvent { - export type InitEvent = IItemEvent - export interface UpdateEvent extends IItemEvent { - getEntity(): import('../entity/IEntity').IEntity; - } - export interface TossedEvent extends IItemEvent { - getEntity(): import('../entity/IEntity').IEntity; - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface PickedUpEvent extends IItemEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface SpawnEvent extends IItemEvent { - getEntity(): import('../entity/IEntity').IEntity; - } - export interface InteractEvent extends IItemEvent { - getType(): number; - getTarget(): import('../entity/IEntity').IEntity; - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface RightClickEvent extends IItemEvent { - getType(): number; - getTarget(): any; - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface AttackEvent extends IItemEvent { - getType(): number; - getTarget(): import('../entity/IEntity').IEntity; - getSwingingEntity(): import('../entity/IEntity').IEntity; - } - export interface StartUsingItem extends IItemEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getDuration(): number; - } - export interface UsingItem extends IItemEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getDuration(): number; - } - export interface StopUsingItem extends IItemEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getDuration(): number; - } - export interface FinishUsingItem extends IItemEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getDuration(): number; - } - export interface BreakItem extends IItemEvent { - getBrokenStack(): import('../item/IItemStack').IItemStack; - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface RepairItem extends IItemEvent { - getLeft(): import('../item/IItemStack').IItemStack; - getRight(): import('../item/IItemStack').IItemStack; - getOutput(): import('../item/IItemStack').IItemStack; - getAnvilBreakChance(): number; - } -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts deleted file mode 100644 index b2274486e..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/ILinkedItemEvent.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface ILinkedItemEvent extends IItemEvent { - - // Nested interfaces - interface VersionChangeEvent extends IItemEvent { - getVersion(): number; - getPreviousVersion(): number; - } - interface BuildEvent extends IItemEvent { - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts deleted file mode 100644 index 742aae138..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/INpcEvent.d.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface INpcEvent extends ICustomNPCsEvent { - getNpc(): import('../entity/ICustomNpc').ICustomNpc; -} - -export namespace INpcEvent { - export interface TimerEvent extends INpcEvent { - getId(): number; - } - export interface CollideEvent extends INpcEvent { - getEntity(): import('../entity/IEntity').IEntity; - } - export interface DamagedEvent extends INpcEvent { - getSource(): import('../entity/IEntity').IEntity; - getDamageSource(): import('../IDamageSource').IDamageSource; - getDamage(): number; - setDamage(damage: number): void; - setClearTarget(bo: boolean): void; - getClearTarget(): boolean; - getType(): string; - } - export interface RangedLaunchedEvent extends INpcEvent { - getTarget(): IEntityLivingBase; - setDamage(damage: number): void; - getDamage(): number; - } - export interface MeleeAttackEvent extends INpcEvent { - getTarget(): IEntityLivingBase; - setDamage(damage: number): void; - getDamage(): number; - } - export interface SwingEvent extends INpcEvent { - getItemStack(): import('../item/IItemStack').IItemStack; - } - export interface KilledEntityEvent extends INpcEvent { - getEntity(): IEntityLivingBase; - } - export interface DiedEvent extends INpcEvent { - getSource(): import('../entity/IEntity').IEntity; - getDamageSource(): import('../IDamageSource').IDamageSource; - getType(): string; - setDroppedItems(droppedItems: import('../item/IItemStack').IItemStack[]): void; - getDroppedItems(): import('../item/IItemStack').IItemStack[]; - setExpDropped(expDropped: number): void; - getExpDropped(): number; - } - export interface InteractEvent extends INpcEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - } - export interface DialogEvent extends INpcEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getDialog(): import('../handler/data/IDialog').IDialog; - getDialogId(): number; - getOptionId(): number; - } - export interface DialogClosedEvent extends INpcEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getDialog(): import('../handler/data/IDialog').IDialog; - getDialogId(): number; - getOptionId(): number; - } - export interface TargetLostEvent extends INpcEvent { - getTarget(): IEntityLivingBase; - getNewTarget(): IEntityLivingBase; - } - export interface TargetEvent extends INpcEvent { - setTarget(entity: IEntityLivingBase): void; - getTarget(): IEntityLivingBase; - } - export interface UpdateEvent extends INpcEvent { - } - export interface InitEvent extends INpcEvent { - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts deleted file mode 100644 index 200eb3567..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPartyEvent.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IPartyEvent { - - // Methods - getParty(): import('../handler/data/IParty').IParty; - getQuest(): import('../handler/data/IQuest').IQuest; - - // Nested interfaces - interface PartyQuestCompletedEvent extends IPartyEvent { - } - interface PartyQuestSetEvent extends IPartyEvent { - } - interface PartyQuestTurnedInEvent extends IPartyEvent { - } - interface PartyInviteEvent extends IPartyEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getPlayerName(): string; - } - interface PartyKickEvent extends IPartyEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getPlayerName(): string; - } - interface PartyLeaveEvent extends IPartyEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; - getPlayerName(): string; - } - interface PartyDisbandEvent extends IPartyEvent { - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts deleted file mode 100644 index e7b6366ca..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IPlayerEvent.d.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IPlayerEvent extends ICustomNPCsEvent { - getPlayer(): import('../entity/IPlayer').IPlayer; -} - -export namespace IPlayerEvent { - export interface ChatEvent extends IPlayerEvent { - setMessage(message: string): void; - getMessage(): string; - } - export interface KeyPressedEvent extends IPlayerEvent { - getKey(): number; - isCtrlPressed(): boolean; - isAltPressed(): boolean; - isShiftPressed(): boolean; - isMetaPressed(): boolean; - keyDown(): boolean; - getKeysDown(): number[]; - } - export interface MouseClickedEvent extends IPlayerEvent { - getButton(): number; - getMouseWheel(): number; - buttonDown(): boolean; - isCtrlPressed(): boolean; - isAltPressed(): boolean; - isShiftPressed(): boolean; - isMetaPressed(): boolean; - getKeysDown(): number[]; - } - export interface PickupXPEvent extends IPlayerEvent { - getAmount(): number; - } - export interface LevelUpEvent extends IPlayerEvent { - getChange(): number; - } - export type LogoutEvent = IPlayerEvent - export type LoginEvent = IPlayerEvent - export type RespawnEvent = IPlayerEvent - export interface ChangedDimension extends IPlayerEvent { - getFromDim(): number; - getToDim(): number; - } - export interface TimerEvent extends IPlayerEvent { - getId(): number; - } - export interface AttackedEvent extends IPlayerEvent { - getDamageSource(): import('../IDamageSource').IDamageSource; - getSource(): import('../entity/IEntity').IEntity; - getDamage(): number; - } - export interface DamagedEvent extends IPlayerEvent { - getDamageSource(): import('../IDamageSource').IDamageSource; - getSource(): import('../entity/IEntity').IEntity; - getDamage(): number; - } - export type LightningEvent = IPlayerEvent - export interface SoundEvent extends IPlayerEvent { - getName(): string; - getPitch(): number; - getVolume(): number; - } - export interface FallEvent extends IPlayerEvent { - getDistance(): number; - } - export type JumpEvent = IPlayerEvent - export interface KilledEntityEvent extends IPlayerEvent { - getEntity(): IEntityLivingBase; - } - export interface DiedEvent extends IPlayerEvent { - getDamageSource(): import('../IDamageSource').IDamageSource; - getType(): string; - getSource(): import('../entity/IEntity').IEntity; - } - export interface RangedLaunchedEvent extends IPlayerEvent { - getBow(): import('../item/IItemStack').IItemStack; - getCharge(): number; - } - export interface AttackEvent extends IPlayerEvent { - getDamageSource(): import('../IDamageSource').IDamageSource; - getTarget(): import('../entity/IEntity').IEntity; - getDamage(): number; - } - export interface DamagedEntityEvent extends IPlayerEvent { - getDamageSource(): import('../IDamageSource').IDamageSource; - getTarget(): import('../entity/IEntity').IEntity; - getDamage(): number; - } - export interface ContainerClosed extends IPlayerEvent { - getContainer(): import('../IContainer').IContainer; - } - export interface ContainerOpen extends IPlayerEvent { - getContainer(): import('../IContainer').IContainer; - } - export interface PickUpEvent extends IPlayerEvent { - getItem(): import('../item/IItemStack').IItemStack; - } - export interface DropEvent extends IPlayerEvent { - getItems(): import('../item/IItemStack').IItemStack[]; - } - export interface TossEvent extends IPlayerEvent { - getItem(): import('../item/IItemStack').IItemStack; - } - export interface InteractEvent extends IPlayerEvent { - getType(): number; - getTarget(): import('../entity/IEntity').IEntity; - } - export interface RightClickEvent extends IPlayerEvent { - getType(): number; - getTarget(): any; - - } - export type UpdateEvent = IPlayerEvent - export type InitEvent = IPlayerEvent - export interface StartUsingItem extends IPlayerEvent { - getItem(): import('../item/IItemStack').IItemStack; - getDuration(): number; - } - export interface UsingItem extends IPlayerEvent { - getItem(): import('../item/IItemStack').IItemStack; - getDuration(): number; - } - export interface StopUsingItem extends IPlayerEvent { - getItem(): import('../item/IItemStack').IItemStack; - getDuration(): number; - } - export interface FinishUsingItem extends IPlayerEvent { - getItem(): import('../item/IItemStack').IItemStack; - getDuration(): number; - } - export interface BreakEvent extends IPlayerEvent { - getBlock(): import('../IBlock').IBlock; - getExp(): number; - } - export interface UseHoeEvent extends IPlayerEvent { - getHoe(): import('../item/IItemStack').IItemStack; - getX(): number; - getY(): number; - getZ(): number; - } - export interface WakeUpEvent extends IPlayerEvent { - setSpawn(): boolean; - } - export interface SleepEvent extends IPlayerEvent { - getX(): number; - getY(): number; - getZ(): number; - } - export interface AchievementEvent extends IPlayerEvent { - getDescription(): string; - } - export interface FillBucketEvent extends IPlayerEvent { - getCurrent(): import('../item/IItemStack').IItemStack; - getFilled(): import('../item/IItemStack').IItemStack; - } - export interface BonemealEvent extends IPlayerEvent { - getBlock(): import('../IBlock').IBlock; - getX(): number; - getY(): number; - getZ(): number; - } - export type RangedChargeEvent = IPlayerEvent - export interface EffectEvent extends IPlayerEvent { - getEffect(): import('../handler/data/IPlayerEffect').IPlayerEffect; - } - export namespace EffectEvent { - export type Added = EffectEvent - export type Ticked = EffectEvent - export interface Removed extends EffectEvent { - hasTimerRunOut(): boolean; - causedByDeath(): boolean; - } - } - - export interface ProfileEvent extends IPlayerEvent { - getProfile(): import('../handler/data/IProfile').IProfile; - getSlot(): number; - isPost(): boolean; - } - export namespace ProfileEvent { - export interface Changed extends ProfileEvent { - getPrevSlot(): number; - } - export type Create = ProfileEvent - export type Removed = ProfileEvent - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts deleted file mode 100644 index 69debbbd0..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IProjectileEvent.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IProjectileEvent extends ICustomNPCsEvent { - - // Methods - getProjectile(): import('../entity/IProjectile').IProjectile; - getSource(): import('../entity/IEntity').IEntity; - - // Nested interfaces - interface UpdateEvent extends IProjectileEvent { - } - interface ImpactEvent extends IProjectileEvent { - getType(): number; - getEntity(): import('../entity/IEntity').IEntity; - getBlock(): import('../IBlock').IBlock; - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts deleted file mode 100644 index a5fa1d3d5..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IQuestEvent.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IQuestEvent extends IPlayerEvent { - - // Methods - getQuest(): import('../handler/data/IQuest').IQuest; - - // Nested interfaces - interface QuestCompletedEvent extends IQuestEvent { - } - interface QuestStartEvent extends IQuestEvent { - } - interface QuestTurnedInEvent extends IQuestEvent { - setExpReward(expReward: number): void; - setItemRewards(itemRewards: import('../item/IItemStack').IItemStack[]): void; - getExpReward(): number; - getItemRewards(): import('../item/IItemStack').IItemStack[]; - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts deleted file mode 100644 index 6c6ad56b3..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/event/IRecipeEvent.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.event - */ - -export interface IRecipeEvent extends IPlayerEvent { - - // Methods - getRecipe(): any; - getItems(): import('../item/IItemStack').IItemStack[]; - isAnvil(): boolean; - setMessage(message: string): void; - getMessage(): string; - getXpCost(): number; - setXpCost(xpCost: number): void; - getMaterialUsage(): number; - setMaterialUsage(materialUsage: number): void; - - // Nested interfaces - interface Pre extends IRecipeEvent { - setMessage(message: string): void; - getMessage(): string; - getXpCost(): number; - setXpCost(xpCost: number): void; - getMaterialUsage(): number; - setMaterialUsage(materialUsage: number): void; - } - interface Post extends IRecipeEvent { - getCraft(): import('../item/IItemStack').IItemStack; - setResult(stack: import('../item/IItemStack').IItemStack): void; - } - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts deleted file mode 100644 index df117e8c9..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/IButton.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.gui - */ - -export interface IButton extends ICustomGuiComponent { - - // Methods - getWidth(): number; - getHeight(): number; - setSize(width: number, height: number): import('./IButton').IButton; - getLabel(): string; - setLabel(text: string): import('./IButton').IButton; - getTexture(): string; - hasTexture(): boolean; - setTexture(texture: string): import('./IButton').IButton; - getTextureX(): number; - getTextureY(): number; - setTextureOffset(textureX: number, textureY: number): import('./IButton').IButton; - setScale(scale: number): void; - getScale(): number; - setEnabled(enabled: boolean): void; - isEnabled(): boolean; - -} diff --git a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts b/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts deleted file mode 100644 index 4c0dd8d86..000000000 --- a/src/main/resources/assets/customnpcs/api/noppes/npcs/api/gui/ICustomGui.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10 - * Package: noppes.npcs.api.gui - */ - -export interface ICustomGui { - - // Methods - getID(): number; - getWidth(): number; - getHeight(): number; - getComponents(): Array Date: Sat, 10 Jan 2026 02:52:20 +0200 Subject: [PATCH 256/337] Moved SubGuiSelectList to CNPC+ --- .../client/gui/script/SubGuiSelectList.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java diff --git a/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java new file mode 100644 index 000000000..e2812ecb2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java @@ -0,0 +1,123 @@ +package noppes.npcs.client.gui.script; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; + +public class SubGuiSelectList extends SubGuiInterface { + private GuiCustomScroll scroll1; + private GuiCustomScroll scroll2; + + public String allName, selectedName; + public List all; + public List selected; + + public SubGuiSelectList(List all, List selected, String allName, String selectedName) { + setBackground("menubg.png"); + xSize = 346; + ySize = 216; + this.allName = allName; + this.selectedName = selectedName; + + if (all == null) + all = new ArrayList<>(); + this.all = all; + + if (selected == null) + selected = new ArrayList<>(); + this.selected = selected; + } + + @Override + public void initGui() { + super.initGui(); + if (scroll1 == null) { + scroll1 = new GuiCustomScroll(this, 0); + scroll1.setSize(140, 180); + } + + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize-22 , guiTop - 17, "X")); + + scroll1.guiLeft = guiLeft + 4; + scroll1.guiTop = guiTop + 14; + this.addScroll(scroll1); + addLabel(new GuiNpcLabel(1, allName, guiLeft + 4, guiTop + 4)); + + if (scroll2 == null) { + scroll2 = new GuiCustomScroll(this, 1); + scroll2.setSize(140, 180); + } + scroll2.guiLeft = guiLeft + 200; + scroll2.guiTop = guiTop + 14; + this.addScroll(scroll2); + addLabel(new GuiNpcLabel(2, selectedName, guiLeft + 200, guiTop + 4)); + + List tempAll = new ArrayList<>(all); + tempAll.removeAll(selected); + scroll1.setList(tempAll); + scroll2.setList(selected); + + addButton(new GuiNpcButton(1, guiLeft + 145, guiTop + 40, 55, 20, ">")); + addButton(new GuiNpcButton(2, guiLeft + 145, guiTop + 62, 55, 20, "<")); + + addButton(new GuiNpcButton(3, guiLeft + 145, guiTop + 90, 55, 20, ">>")); + addButton(new GuiNpcButton(4, guiLeft + 145, guiTop + 112, 55, 20, "<<")); + + addButton(new GuiNpcButton(66, guiLeft + 260, guiTop + 194, 60, 20, "gui.done")); + } + + protected void actionPerformed(GuiButton guibutton) { + GuiNpcButton button = (GuiNpcButton) guibutton; + + if (button.id == -1) { + close(); + return; + } + + if (button.id == 1) { + if (scroll1.hasSelected()) { + selected.add(scroll1.getSelected()); + + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + } + if (button.id == 2) { + if (scroll2.hasSelected()) { + selected.remove(scroll2.getSelected()); + scroll2.selected = -1; + initGui(); + } + } + if (button.id == 3) { + selected.clear(); + for (String type : all) + selected.add(type); + + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + if (button.id == 4) { + selected.clear(); + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + if (button.id == 66) { + close(); + } + } + + @Override + public void save() { + + } +} From 95f550df0b028bcda0093a7e6fb9d0fed208db4d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 02:52:38 +0200 Subject: [PATCH 257/337] Moved SubGuiSelectList to CNPC+ --- src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index 874beca4b..b8d222438 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -1,6 +1,5 @@ package noppes.npcs.client.gui.script; -import kamkeel.npcdbc.client.gui.component.SubGuiSelectList; import noppes.npcs.janino.JaninoScript; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; From 03935502cce55fce406aab7b525fd57ea6d11611 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 03:53:26 +0200 Subject: [PATCH 258/337] Pushed local dts plugin with settings.gradle configured to use locally --- gradle-plugins/.gitignore | 6 + gradle-plugins/README.md | 69 + gradle-plugins/build.gradle | 50 + gradle-plugins/settings.gradle | 1 + .../groovy/dts/GenerateTypeScriptTask.groovy | 137 ++ .../dts/JavaToTypeScriptConverter.groovy | 1223 +++++++++++++++++ .../dts/TypeScriptGeneratorPlugin.groovy | 17 + .../dts.typescript-generator.properties | 1 + settings.gradle | 19 +- 9 files changed, 1515 insertions(+), 8 deletions(-) create mode 100644 gradle-plugins/.gitignore create mode 100644 gradle-plugins/README.md create mode 100644 gradle-plugins/build.gradle create mode 100644 gradle-plugins/settings.gradle create mode 100644 gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy create mode 100644 gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy create mode 100644 gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy create mode 100644 gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties diff --git a/gradle-plugins/.gitignore b/gradle-plugins/.gitignore new file mode 100644 index 000000000..94cac27cf --- /dev/null +++ b/gradle-plugins/.gitignore @@ -0,0 +1,6 @@ +/.gradle +/build +/out +/bin +/.idea +/.vscode \ No newline at end of file diff --git a/gradle-plugins/README.md b/gradle-plugins/README.md new file mode 100644 index 000000000..78f1a1091 --- /dev/null +++ b/gradle-plugins/README.md @@ -0,0 +1,69 @@ +# dts-gradle-plugin + +This repo contains the Gradle plugin for generating TypeScript `.d.ts` files for the CustomNPC-Plus API. + +These files are used for the generation of auto-completion suggestions and type/interface documentation when scripting +in the Script Editor in-game. + +### Implementation using JitPack + +Add JitPack to the buildscript and depend on the plugin JAR. Using `main-SNAPSHOT` will build the latest `main` commit; +for reproducible builds use a tag or commit hash instead. + +`settings.gradle` +```gradle +pluginManagement { + resolutionStrategy { + eachPlugin { + if(requested.id.toString() == "dts.typescript-generator") { + useModule("com.github.bigguy345:dts-gradle-plugin:main-SNAPSHOT") + } + } + } + + + repositories { + maven { url "https://jitpack.io" } + gradlePluginPortal() + mavenCentral() + mavenLocal() + } +} +``` + + + + +`build.gradle`: + +```gradle +plugins { + id 'dts.typescript-generator' +} + +// ============================================================================ +// TypeScript Definition Generation Task +// Generates .d.ts files from Java API sources for scripting IDE support +// ============================================================================ +// TypeScript plugin is applied above in the main plugins block + +tasks.named("generateTypeScriptDefinitions").configure { + // Source directories containing the Java API code + sourceDirectories = ['src/main/java'] + + // Packages in source directories to generate .d.ts files for + apiPackages = ['noppes.npcs.api'] as Set + + // Output directory for the generated .d.ts files + // Must be within resources/assets/${modId}/api to be detected by CNPC+ + outputDirectory = "src/main/resources/assets/${modId}/api" + + // Whether to clean old generated files before regenerating + cleanOutputFirst = true +} + + +// Optional: To ensure definitions are generated on processing resources on jar build +// But in most cases, you may want to run the task manually when needed +// processResources.dependsOn generateTypeScriptDefinitions +``` diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle new file mode 100644 index 000000000..0deda7acc --- /dev/null +++ b/gradle-plugins/build.gradle @@ -0,0 +1,50 @@ +plugins { + id 'groovy-gradle-plugin' + id 'maven-publish' +} + +group = 'dts' +version = '1.0.0' + +repositories { + mavenCentral() +} + +dependencies { + implementation gradleApi() + implementation localGroovy() +} + +// Configure source sets if building as standalone +sourceSets { + main { + groovy { + srcDirs = ['src/main/groovy'] + } + resources { + srcDirs = ['src/main/resources'] + } + } +} + +gradlePlugin { + plugins { + typescriptGenerator { + id = 'dts.typescript-generator' + implementationClass = 'dts.TypeScriptGeneratorPlugin' + displayName = 'TypeScript Definition Generator' + description = 'Generates TypeScript definition files from Java API sources' + } + } +} + +publishing { + repositories { + mavenLocal() + } +} + +// Handle duplicate resources +tasks.processResources { + duplicatesStrategy = 'include' +} diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle new file mode 100644 index 000000000..14cad52d4 --- /dev/null +++ b/gradle-plugins/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'dts-gradle-plugin' diff --git a/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy b/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy new file mode 100644 index 000000000..31a06c609 --- /dev/null +++ b/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy @@ -0,0 +1,137 @@ +package dts + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.file.FileCollection + +/** + * Gradle task that generates TypeScript definition (.d.ts) files from Java API sources. + * + * Usage in build.gradle: + * + * tasks.withType(GenerateTypeScriptTask).configureEach { + * sourceDirectories = ['src/api/java'] // Strings are converted to Files + * outputDirectory = 'src/main/resources/${modid}/api' // String converted to File + * apiPackages = ['noppes.npcs.api', 'kamkeel.npcdbc.api'] + * } + */ +abstract class GenerateTypeScriptTask extends DefaultTask { + + // Accept Object types (String or File) with @Internal annotation + @Internal + List sourceDirectories = [] + + @Internal + Object outputDirectory + + @Input + Set apiPackages = [] + + @Input + Boolean cleanOutputFirst = false + + @Input + List excludePatterns = [] + + // Provide task input/output properties that Gradle can validate + @InputFiles + protected List getResolvedSourceDirectories() { + return sourceDirectories.collect { obj -> + if (obj instanceof File) return obj + String pathStr = obj.toString() + if (pathStr.contains('${modid}')) { + String modid = project.archivesBaseName ?: 'mod' + pathStr = pathStr.replace('${modid}', modid) + } + File f = new File(pathStr) + if (!f.isAbsolute()) { + f = new File(project.projectDir, pathStr) + } + return f + } + } + + @OutputDirectory + protected File getResolvedOutputDirectory() { + def obj = outputDirectory + if (obj instanceof File) return obj + String pathStr = obj.toString() + if (pathStr.contains('${modid}')) { + String modid = project.archivesBaseName ?: 'mod' + pathStr = pathStr.replace('${modid}', modid) + } + File f = new File(pathStr) + if (!f.isAbsolute()) { + f = new File(project.projectDir, pathStr) + } + return f + } + + /** + * Converts a value (String or File) to a File object. + * Supports special tokens like ${modid}. + */ + // conversion logic inlined into getters to avoid calling private helper at configuration time + + GenerateTypeScriptTask() { + group = 'api' + description = 'Generates TypeScript definition files from Java API sources' + } + + @TaskAction + void generate() { + List srcDirs = getResolvedSourceDirectories() + File outDir = getResolvedOutputDirectory() + + logger.lifecycle("=".multiply(60)) + logger.lifecycle("Generating TypeScript definitions...") + logger.lifecycle("=".multiply(60)) + + if (srcDirs.isEmpty()) { + logger.warn("No source directories specified!") + return + } + + // Validate directories + srcDirs.each { dir -> + if (!dir.exists()) { + logger.warn("Source directory does not exist: ${dir}") + } else { + logger.lifecycle("Source: ${dir}") + } + } + + logger.lifecycle("Output: ${outDir}") + logger.lifecycle("API Packages: ${apiPackages}") + + // Clean output if requested + if (cleanOutputFirst && outDir.exists()) { + logger.lifecycle("Cleaning output directory...") + outDir.eachFileRecurse { file -> + if (file.name.endsWith('.d.ts') && !file.name.equals('minecraft-raw.d.ts') && !file.name.equals('forge-events-raw.d.ts')) { + file.delete() + } + } + } + + // Create converter and process + JavaToTypeScriptConverter converter = new JavaToTypeScriptConverter(outDir, apiPackages) + + List validDirs = srcDirs.findAll { it.exists() } + if (validDirs.isEmpty()) { + logger.error("No valid source directories found!") + return + } + + converter.processDirectories(validDirs, logger) + + logger.lifecycle("=".multiply(60)) + logger.lifecycle("TypeScript definition generation complete!") + logger.lifecycle("=".multiply(60)) + } +} diff --git a/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy b/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy new file mode 100644 index 000000000..5b73f36ad --- /dev/null +++ b/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy @@ -0,0 +1,1223 @@ +package dts + +import groovy.transform.TypeChecked +import org.gradle.api.logging.Logger + +import java.util.regex.Pattern +import java.util.regex.Matcher + +/** + * Parses Java source files and converts them to TypeScript definition (.d.ts) files. + * Handles interfaces, classes, nested types, generics, and JavaDoc preservation. + */ +class JavaToTypeScriptConverter { + + // Common Java type mappings to TypeScript + private static final Map PRIMITIVE_MAPPINGS = [ + 'void': 'void', + 'boolean': 'boolean', + 'byte': 'number', + 'short': 'number', + 'int': 'number', + 'long': 'number', + 'float': 'number', + 'double': 'number', + 'char': 'string', + 'String': 'string', + 'Object': 'any', + 'Boolean': 'boolean', + 'Byte': 'number', + 'Short': 'number', + 'Integer': 'number', + 'Long': 'number', + 'Float': 'number', + 'Double': 'number', + 'Character': 'string', + 'Number': 'number', + ] + + // Java functional interface mappings + private static final Map FUNCTIONAL_MAPPINGS = [ + 'Consumer': '(arg: %s) => void', + 'Supplier': '() => %s', + 'Function': '(arg: %s) => %s', + 'Predicate': '(arg: %s) => boolean', + 'BiConsumer': '(arg1: %s, arg2: %s) => void', + 'BiFunction': '(arg1: %s, arg2: %s) => %s', + 'Runnable': '() => void', + 'Callable': '() => %s', + ] + + // Packages that are part of API (generate imports to local .d.ts) + private Set apiPackages = [] as Set + + // Base output directory for generated files + private File outputDir + + // Track all generated types for index.d.ts + private List generatedTypes = [] + + // Track hooks for hooks.d.ts + private Map> hooks = [:] + + private Logger logger; + + JavaToTypeScriptConverter(File outputDir, Set apiPackages) { + this.outputDir = outputDir + this.apiPackages = apiPackages + } + + /** + * Process all Java files in the given directories + */ + void processDirectories(List sourceDirs, Logger logger) { + this.logger = logger + sourceDirs.each { dir -> + if (dir.exists()) { + processDirectory(dir, dir) + } + } + + // Generate index.d.ts + generateIndexFile() + + // Generate hooks.d.ts + generateHooksFile() + } + + private void processDirectory(File dir, File baseDir) { + dir.eachFileRecurse { file -> + if (file.name.endsWith('.java') && !file.name.equals('package-info.java')) { + // Early filtering - check if file matches any API package + String relativePath = baseDir.toPath().relativize(file.toPath()).toString() + String packagePath = relativePath.replace('\\', '/').replace('.java', '').replace('/', '.') + + // Only process if the package starts with one of the apiPackages + boolean shouldProcess = apiPackages.any { apiPkg -> packagePath.startsWith(apiPkg) } + + if (shouldProcess) + processJavaFile(file, baseDir) + } + } + } + + + + /** + * Process a single Java file + */ + void processJavaFile(File javaFile, File baseDir) { + String content = javaFile.text + ParsedJavaFile parsed = parseJavaFile(content) + + if (parsed == null) { + logger.warn("Failed to parse ${javaFile.name} - parseJavaFile returned null") + return + } + if (parsed.types.isEmpty()) { + logger.warn("No types extracted from ${javaFile.name} - package: ${parsed.packageName}") + return + } + // logger.lifecycle("Successfully parsed ${javaFile.name}: ${parsed.types.size()} type(s) found: ${parsed.types*.name}") + + // Determine output path + String relativePath = baseDir.toPath().relativize(javaFile.toPath()).toString() + String dtsPath = relativePath.replace('.java', '.d.ts').replace('\\', '/') + File outputFile = new File(outputDir, dtsPath) + + // Generate TypeScript content + String tsContent = generateTypeScript(parsed, dtsPath) + + // Write file + outputFile.parentFile.mkdirs() + outputFile.text = tsContent + + // Track for index generation + parsed.types.each { type -> + generatedTypes << new TypeInfo( + name: type.name, + packageName: parsed.packageName, + filePath: dtsPath, + isClass: type.isClass, + isInterface: type.isInterface, + extendsType: type.extendsType + ) + + // Track nested types + type.nestedTypes.each { nested -> + generatedTypes << new TypeInfo( + name: "${type.name}.${nested.name}", + packageName: parsed.packageName, + filePath: dtsPath, + isClass: nested.isClass, + isInterface: nested.isInterface, + parentType: type.name + ) + } + } + + // Collect hooks from event interfaces + collectHooks(parsed) + } + + /** + * Parse a Java file into structured data + */ + ParsedJavaFile parseJavaFile(String content) { + ParsedJavaFile result = new ParsedJavaFile() + + // Extract package + def packageMatcher = content =~ /package\s+([\w.]+)\s*;/ + if (packageMatcher.find()) { + result.packageName = packageMatcher.group(1) + } + + // Extract imports + def importMatcher = content =~ /import\s+([\w.*]+)\s*;/ + while (importMatcher.find()) { + result.imports << importMatcher.group(1) + } + + // Parse types (interfaces and classes) + parseTypes(content, result) + + return result + } + + private void parseTypes(String content, ParsedJavaFile result) { + // Match ONLY top-level (public) interface or class declarations + // Top-level types MUST have 'public' modifier in Java + // Use a simpler pattern first, then manually extract type parameters + // Handle modifiers like abstract, final, static (in any order) + def typePattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?public\s+(?:(?:abstract|final|static)\s+)*(interface|class)\s+(\w+)/ + + def matcher = content =~ typePattern + while (matcher.find()) { + JavaType type = new JavaType() + type.jsdoc = matcher.group(1)?.trim() + type.isInterface = matcher.group(2) == 'interface' + type.isClass = matcher.group(2) == 'class' + type.name = matcher.group(3) + + // Manually extract type parameters with balanced bracket matching + int afterName = matcher.end() + String remainder = content.substring(afterName) + + // Check if there are type parameters + if (remainder.trim().startsWith('<')) { + int startIndex = remainder.indexOf('<') + int depth = 0 + int endIndex = -1 + + for (int i = startIndex; i < remainder.length(); i++) { + char c = remainder.charAt(i) + if (c == '<') { + depth++ + } else if (c == '>') { + depth-- + if (depth == 0) { + endIndex = i + break + } + } + } + + if (endIndex > startIndex) { + type.typeParams = remainder.substring(startIndex + 1, endIndex).trim() + // Parse type parameters with full class names + type.parsedTypeParams = parseTypeParams(type.typeParams, result) + remainder = remainder.substring(endIndex + 1) + } + } + + // Now parse extends and implements + def extendsPattern = ~/\s+extends\s+([\w.<>,\s]+?)(?:\s+implements|\s*\{)/ + def extendsMatcher = remainder =~ extendsPattern + if (extendsMatcher.find()) { + type.extendsType = extendsMatcher.group(1)?.trim() + } + + def implementsPattern = ~/\s+implements\s+([\w.<>,\s]+?)\s*\{/ + def implementsMatcher = remainder =~ implementsPattern + if (implementsMatcher.find()) { + type.implementsTypes = implementsMatcher.group(1)?.split(',')?.collect { it.trim() } ?: [] + } else { + type.implementsTypes = [] + } + + // Find the opening brace for the body + int braceIndex = content.indexOf('{', afterName) + if (braceIndex == -1) { + continue // No body found, skip this type + } + + // Find the body of this type + int bodyStart = braceIndex + int bodyEnd = findMatchingBrace(content, bodyStart) + if (bodyEnd > bodyStart) { + String body = content.substring(bodyStart + 1, bodyEnd) + + // Parse methods - pass original body for JSDoc extraction + type.methods = parseMethods(body) + + // Parse nested types + type.nestedTypes = parseNestedTypes(body, type.name) + + // Parse fields (for classes) + if (type.isClass) { + type.fields = parseFields(body) + } + } + + result.types << type + } + } + + /** + * Remove nested type bodies from a string so we only parse top-level methods + */ + private String removeNestedTypeBodies(String body) { + StringBuilder result = new StringBuilder() + int depth = 0 + boolean inNestedType = false + int nestedStart = -1 + + // Find nested type declarations and remove their bodies + def nestedPattern = ~/(?:public\s+)?(?:static\s+)?(interface|class)\s+\w+/ + + int i = 0 + while (i < body.length()) { + char c = body.charAt(i) + + if (c == '{') { + if (!inNestedType) { + // Check if this brace starts a nested type + String before = body.substring(Math.max(0, i - 100), i) + if (before =~ /(?:public\s+)?(?:static\s+)?(?:interface|class)\s+\w+[^{]*$/) { + inNestedType = true + nestedStart = i + depth = 1 + i++ + continue + } + } + if (inNestedType) { + depth++ + } + } else if (c == '}') { + if (inNestedType) { + depth-- + if (depth == 0) { + inNestedType = false + // Don't add the nested type body to result + i++ + continue + } + } + } + + if (!inNestedType) { + result.append(c) + } + i++ + } + + return result.toString() + } + + private List parseMethods(String body) { + List methods = [] + + // Remove nested type bodies first to only get top-level methods + String topLevelBody = removeNestedTypeBodies(body) + + // Match method signatures - handles complex generics + // Capture JSDoc in group 1, returnType in group 2, methodName in group 3, params in group 4 + def methodPattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:abstract\s+)?(?:default\s+)?(?:synchronized\s+)?(?:final\s+)?(?:<[^>]+>\s+)?(\w[\w.<>,\[\]\s]*?)\s+(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?[;{]/ + + def matcher = topLevelBody =~ methodPattern + while (matcher.find()) { + String jsdoc = matcher.group(1)?.trim() + String returnType = matcher.group(2).trim() + String methodName = matcher.group(3) + + // Skip constructors - where return type is a visibility modifier + // or the method name matches the class name (which we'd need to track) + if (['public', 'protected', 'private', 'abstract', 'static', 'final', 'synchronized', 'native', 'strictfp'].contains(returnType)) { + continue + } + + JavaMethod method = new JavaMethod() + method.returnType = returnType + method.name = methodName + method.parameters = parseParameters(matcher.group(4)) + method.jsdoc = jsdoc + + methods << method + } + + return methods + } + + private List parseFields(String body) { + List fields = [] + + // Remove nested type bodies first + String topLevelBody = removeNestedTypeBodies(body) + + // Capture JSDoc in group 1, visibility in group 2, fieldType in group 3, fieldName in group 4 + def fieldPattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?(public\s+|protected\s+|private\s+)(?:static\s+)?(?:final\s+)?(\w[\w.<>,\[\]]*)\s+(\w+)\s*[;=]/ + + def matcher = topLevelBody =~ fieldPattern + while (matcher.find()) { + String jsdoc = matcher.group(1)?.trim() + String fieldType = matcher.group(3).trim() + String fieldName = matcher.group(4) + + // Skip Java keywords that might be mismatched + if (['return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'throw', 'try', 'catch', 'finally', 'new', 'this', 'super'].contains(fieldType)) { + continue + } + + JavaField field = new JavaField() + field.type = fieldType + field.name = fieldName + field.jsdoc = jsdoc + + fields << field + } + + return fields + } + + private List parseNestedTypes(String body, String parentName) { + List nestedTypes = [] + + // Capture JSDoc in group 1, interface/class in group 2, name in group 3, typeParams in group 4, extends in group 5 + def nestedPattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+)?(?:static\s+)?(interface|class)\s+(\w+)(?:<([^>]+)>)?(?:\s+extends\s+([\w.<>,\s]+))?\s*\{/ + + // We need to track position and skip over bodies of found types to avoid finding nested-nested types + int searchStart = 0 + def matcher = nestedPattern.matcher(body) + + while (matcher.find(searchStart)) { + JavaType nested = new JavaType() + nested.jsdoc = matcher.group(1)?.trim() + nested.isInterface = matcher.group(2) == 'interface' + nested.isClass = matcher.group(2) == 'class' + nested.name = matcher.group(3) + nested.typeParams = matcher.group(4) + nested.extendsType = matcher.group(5)?.trim() + + int bodyStart = matcher.end() - 1 + int bodyEnd = findMatchingBrace(body, bodyStart) + if (bodyEnd > bodyStart) { + String nestedBody = body.substring(bodyStart + 1, bodyEnd) + nested.methods = parseMethods(nestedBody) + // Recursively parse nested types within this nested type + nested.nestedTypes = parseNestedTypes(nestedBody, nested.name) + + // Skip past the entire body of this type for the next search + searchStart = bodyEnd + 1 + } else { + // If we couldn't find the matching brace, move past this match + searchStart = matcher.end() + } + + nestedTypes << nested + } + + return nestedTypes + } + + private List parseParameters(String paramsStr) { + List params = [] + if (paramsStr == null || paramsStr.trim().isEmpty()) return params + + // Handle complex generic parameters + List paramParts = splitParameters(paramsStr) + + paramParts.each { part -> + part = part.trim() + if (part.isEmpty()) return + + // Handle varargs + boolean isVarargs = part.contains('...') + part = part.replace('...', '[]') + + // Split type and name + int lastSpace = part.lastIndexOf(' ') + if (lastSpace > 0 && lastSpace < part.length() - 1) { + JavaParameter param = new JavaParameter() + param.type = part.substring(0, lastSpace).trim() + param.name = part.substring(lastSpace + 1).trim() + param.isVarargs = isVarargs + params << param + } else if (lastSpace == -1 && !part.isEmpty()) { + // No space found - might be a single token, skip it + // This can happen with malformed or unusual parameter declarations + } + } + + return params + } + + /** + * Split parameters handling nested generics + */ + private List splitParameters(String params) { + List result = [] + int depth = 0 + StringBuilder current = new StringBuilder() + + params.each { ch -> + if (ch == '<') depth++ + else if (ch == '>') depth-- + else if (ch == ',' && depth == 0) { + result << current.toString() + current = new StringBuilder() + return + } + current.append(ch) + } + + if (current.length() > 0) { + result << current.toString() + } + + return result + } + + private String extractJsDocBefore(String content, int position) { + // Look backwards for JSDoc + String before = content.substring(0, position) + def jsdocMatcher = before =~ /\/\*\*[\s\S]*?\*\/\s*$/ + if (jsdocMatcher.find()) { + return jsdocMatcher.group(0).trim() + } + return null + } + + private int findMatchingBrace(String content, int start) { + int depth = 0 + for (int i = start; i < content.length(); i++) { + char c = content.charAt(i) + if (c == '{') depth++ + else if (c == '}') { + depth-- + if (depth == 0) return i + } + } + return -1 + } + + private String removeBlockComments(String content) { + // Remove JSDoc and block comments for structure parsing + return content.replaceAll(/\/\*[\s\S]*?\*\//, '') + } + + /** + * Generate TypeScript content from parsed Java + */ + String generateTypeScript(ParsedJavaFile parsed, String currentPath) { + StringBuilder sb = new StringBuilder() + + // Header comment + sb.append('/**\n') + sb.append(' * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10\n') + sb.append(" * Package: ${parsed.packageName}\n") + sb.append(' */\n\n') + + parsed.types.each { type -> + generateType(sb, type, parsed, currentPath, '') + } + + return sb.toString() + } + + private void generateType(StringBuilder sb, JavaType type, ParsedJavaFile parsed, String currentPath, String indent) { + // Build a set of type parameter names for this type (e.g., "T", "U", etc.) + Set typeParamNames = type.parsedTypeParams?.collect { it.name }?.toSet() ?: [] as Set + + // JSDoc + if (type.jsdoc) { + sb.append(convertJsDoc(type.jsdoc, indent)) + sb.append('\n') + } + + // Type declaration + String keyword = type.isClass ? 'export class' : 'export interface' + sb.append("${indent}${keyword} ${type.name}") + + // Type parameters with full class name bounds in JSDoc comment + if (type.typeParams) { + sb.append("<${convertTypeParams(type.typeParams, type.parsedTypeParams)}>") + } + + // Extends - skip if extending itself + if (type.extendsType && type.extendsType != type.name) { + sb.append(" extends ${convertType(type.extendsType, parsed, currentPath, typeParamNames)}") + } + + sb.append(' {\n') + + // Methods - compact format, no comments + type.methods.each { method -> + generateMethod(sb, method, parsed, currentPath, indent + ' ', typeParamNames) + } + + // Fields (for classes) + type.fields.each { field -> + generateField(sb, field, parsed, currentPath, indent + ' ', typeParamNames) + } + + sb.append("${indent}}\n") + + // Nested types as namespace + if (!type.nestedTypes.isEmpty()) { + sb.append("\n${indent}export namespace ${type.name} {\n") + type.nestedTypes.each { nested -> + // Always generate as interface, even if empty + // Empty interfaces with extends are important for type hierarchy + generateNestedType(sb, nested, type.name, parsed, currentPath, indent + ' ') + } + sb.append("${indent}}\n") + } + sb.append('\n') + } + + /** + * Generate a nested type (interface/class within a namespace) + * Recursively handles nested types that themselves have nested types + */ + private void generateNestedType(StringBuilder sb, JavaType type, String parentTypeName, ParsedJavaFile parsed, String currentPath, String indent) { + // Build a set of type parameter names for this nested type + Set typeParamNames = type.parsedTypeParams?.collect { it.name }?.toSet() ?: [] as Set + + // JSDoc + if (type.jsdoc) { + sb.append(convertJsDoc(type.jsdoc, indent)) + sb.append('\n') + } + + // Type declaration + String keyword = type.isClass ? 'export class' : 'export interface' + sb.append("${indent}${keyword} ${type.name}") + + // Type parameters with full class name bounds + if (type.typeParams) { + sb.append("<${convertTypeParams(type.typeParams, type.parsedTypeParams)}>") + } + + // Extends - handle parent type reference specially, skip if extending itself + if (type.extendsType && type.extendsType != type.name) { + String extendsRef = convertTypeForNested(type.extendsType, parentTypeName, parsed, currentPath, typeParamNames) + sb.append(" extends ${extendsRef}") + } + + sb.append(' {\n') + + // Methods - compact format, no comments + type.methods.each { method -> + generateMethod(sb, method, parsed, currentPath, indent + ' ', typeParamNames) + } + + sb.append("${indent}}\n") + + // If this nested type also has nested types, create a namespace for them + if (!type.nestedTypes.isEmpty()) { + sb.append("${indent}export namespace ${type.name} {\n") + type.nestedTypes.each { nested -> + // Always generate as interface, even if empty + // Empty interfaces with extends are important for type hierarchy + generateNestedType(sb, nested, type.name, parsed, currentPath, indent + ' ') + } + sb.append("${indent}}\n") + } + } + + /** + * Convert type reference for nested types - handle parent type specially + */ + private String convertTypeForNested(String javaType, String parentTypeName, ParsedJavaFile parsed, String currentPath, Set typeParamNames) { + if (javaType == null || javaType.isEmpty()) return 'any' + + javaType = javaType.trim() + + // If the type is the parent type name, use it directly (not as import from same file) + if (javaType == parentTypeName) { + return parentTypeName + } + + // Otherwise use normal conversion + return convertType(javaType, parsed, currentPath, typeParamNames) + } + + private void generateMethod(StringBuilder sb, JavaMethod method, ParsedJavaFile parsed, String currentPath, String indent, Set typeParamNames) { + // JSDoc + if (method.jsdoc) { + sb.append(convertJsDoc(method.jsdoc, indent)) + sb.append('\n') + } + + sb.append("${indent}${method.name}(") + + // Parameters + List paramStrs = method.parameters.collect { param -> + String tsType = convertType(param.type, parsed, currentPath, typeParamNames) + if (param.isVarargs) { + return "...${param.name}: ${tsType}" + } + return "${param.name}: ${tsType}" + } + sb.append(paramStrs.join(', ')) + + sb.append('): ') + sb.append(convertType(method.returnType, parsed, currentPath, typeParamNames)) + sb.append(';\n') + } + + private void generateField(StringBuilder sb, JavaField field, ParsedJavaFile parsed, String currentPath, String indent, Set typeParamNames) { + if (field.jsdoc) { + sb.append(convertJsDoc(field.jsdoc, indent)) + sb.append('\n') + } + sb.append("${indent}${field.name}: ${convertType(field.type, parsed, currentPath, typeParamNames)};\n") + } + + /** + * Convert Java type to TypeScript type + * @param typeParamNames Set of type parameter names from the enclosing type (e.g., "T", "U") + */ + String convertType(String javaType, ParsedJavaFile parsed, String currentPath, Set typeParamNames = [] as Set) { + if (javaType == null || javaType.isEmpty()) return 'any' + + javaType = javaType.trim() + + // Check if it's a type parameter (like T, U, etc.) + if (typeParamNames.contains(javaType)) { + return javaType + } + + // Check primitives first + if (PRIMITIVE_MAPPINGS.containsKey(javaType)) { + return PRIMITIVE_MAPPINGS[javaType] + } + + // Handle arrays + if (javaType.endsWith('[]') && javaType.length() > 2) { + String baseType = javaType.substring(0, javaType.length() - 2) + return convertType(baseType, parsed, currentPath, typeParamNames) + '[]' + } + + // Handle generics + if (javaType.contains('<')) { + return convertGenericType(javaType, parsed, currentPath, typeParamNames) + } + + // Handle functional interfaces from java.util.function + if (FUNCTIONAL_MAPPINGS.containsKey(javaType)) { + return 'Function' // Simplified - will be expanded in generic handling + } + + // Check if it is an API type (needs import) + String importPath = resolveImportPath(javaType, parsed, currentPath) + if (importPath != null) { + return "import('${importPath}').${javaType}" + } + + // Check if it is a java.* type + String fullType = resolveFullType(javaType, parsed) + if (fullType != null && fullType.startsWith('java.')) { + return "Java.${fullType}" + } + + // Default - return as is (might be a type parameter like T, or unknown type) + return javaType + } + + private String convertGenericType(String type, ParsedJavaFile parsed, String currentPath, Set typeParamNames = [] as Set) { + int ltIndex = type.indexOf('<') + int gtIndex = type.lastIndexOf('>') + if (ltIndex == -1 || gtIndex == -1 || ltIndex >= gtIndex) { + // Malformed generic, return as-is + return type + } + String baseType = type.substring(0, ltIndex).trim() + String genericPart = type.substring(ltIndex + 1, gtIndex).trim() + + // Handle common collections + switch (baseType) { + case 'List': + case 'ArrayList': + case 'LinkedList': + case 'Collection': + case 'Set': + case 'HashSet': + case 'Queue': + return convertType(genericPart, parsed, currentPath, typeParamNames) + '[]' + + case 'Map': + case 'HashMap': + case 'LinkedHashMap': + List parts = splitGenericParams(genericPart) + if (parts.size() >= 2) { + String keyType = convertType(parts[0], parsed, currentPath, typeParamNames) + String valueType = convertType(parts[1], parsed, currentPath, typeParamNames) + return "Record<${keyType}, ${valueType}>" + } + return 'Record' + + case 'Optional': + return convertType(genericPart, parsed, currentPath, typeParamNames) + ' | null' + + // Functional interfaces + case 'Consumer': + return "(arg: ${convertType(genericPart, parsed, currentPath, typeParamNames)}) => void" + + case 'Supplier': + return "() => ${convertType(genericPart, parsed, currentPath, typeParamNames)}" + + case 'Function': + List funcParts = splitGenericParams(genericPart) + if (funcParts.size() >= 2) { + return "(arg: ${convertType(funcParts[0], parsed, currentPath, typeParamNames)}) => ${convertType(funcParts[1], parsed, currentPath, typeParamNames)}" + } + return '(arg: any) => any' + + case 'Predicate': + return "(arg: ${convertType(genericPart, parsed, currentPath, typeParamNames)}) => boolean" + + case 'BiConsumer': + List biParts = splitGenericParams(genericPart) + if (biParts.size() >= 2) { + return "(arg1: ${convertType(biParts[0], parsed, currentPath, typeParamNames)}, arg2: ${convertType(biParts[1], parsed, currentPath, typeParamNames)}) => void" + } + return '(arg1: any, arg2: any) => void' + + case 'BiFunction': + List biFuncParts = splitGenericParams(genericPart) + if (biFuncParts.size() >= 3) { + return "(arg1: ${convertType(biFuncParts[0], parsed, currentPath, typeParamNames)}, arg2: ${convertType(biFuncParts[1], parsed, currentPath, typeParamNames)}) => ${convertType(biFuncParts[2], parsed, currentPath, typeParamNames)}" + } + return '(arg1: any, arg2: any) => any' + + default: + // Regular generic type + String convertedBase = convertType(baseType, parsed, currentPath, typeParamNames) + List convertedParams = splitGenericParams(genericPart).collect { + convertType(it, parsed, currentPath, typeParamNames) + } + // For import types, we cannot add generics easily, so simplify + if (convertedBase.startsWith('import(')) { + return convertedBase + } + return "${convertedBase}<${convertedParams.join(', ')}>" + } + } + + private List splitGenericParams(String params) { + List result = [] + int depth = 0 + StringBuilder current = new StringBuilder() + + params.each { ch -> + if (ch == '<') depth++ + else if (ch == '>') depth-- + else if (ch == ',' && depth == 0) { + result << current.toString().trim() + current = new StringBuilder() + return + } + current.append(ch) + } + + if (current.length() > 0) { + result << current.toString().trim() + } + + return result + } + + /** + * Convert Java type parameters to TypeScript + * Outputs format like: T extends EntityPlayerMP /`*` net.minecraft.entity.player.EntityPlayerMP `*`/ + * This allows runtime parsing to extract the full Java class name for the type bound. + */ + private String convertTypeParams(String typeParams, List parsedParams) { + if (typeParams == null || typeParams.isEmpty()) return typeParams + if (parsedParams == null || parsedParams.isEmpty()) return typeParams + + // Build result by iterating over parsed params + List convertedParams = [] + for (TypeParamInfo info in parsedParams) { + StringBuilder sb = new StringBuilder() + sb.append(info.name) + + if (info.boundType != null) { + sb.append(" extends ") + sb.append(info.boundType) + + // Add full class name as JSDoc-style comment if available + if (info.fullBoundType != null) { + sb.append(" /* ${info.fullBoundType} */") + } + } + + convertedParams << sb.toString() + } + + return convertedParams.join(', ') + } + + private String resolveImportPath(String typeName, ParsedJavaFile parsed, String currentPath) { + // Check imports for this type + String fullType = resolveFullType(typeName, parsed) + + if (fullType == null) return null + + // Check if it is an API type + int lastDotIndex = fullType.lastIndexOf('.') + if (lastDotIndex == -1) return null + String packagePrefix = fullType.substring(0, lastDotIndex) + if (apiPackages.any { fullType.startsWith(it) }) { + // Calculate relative path + String typeFilePath = fullType.replace('.', '/') + '.d.ts' + return calculateRelativePath(currentPath, typeFilePath) + } + + return null + } + + private String resolveFullType(String typeName, ParsedJavaFile parsed) { + // Check explicit imports + String explicitImport = parsed.imports.find { it.endsWith(".${typeName}") } + if (explicitImport) return explicitImport + + // Check wildcard imports + parsed.imports.each { imp -> + if (imp.endsWith('.*')) { + // Would need classpath to resolve, skip for now + } + } + + // Same package + return "${parsed.packageName}.${typeName}" + } + + /** + * Parse type parameters like "T extends EntityPlayerMP" into structured TypeParamInfo + * with full class names resolved from imports + */ + private List parseTypeParams(String typeParams, ParsedJavaFile parsed) { + List result = [] + if (typeParams == null || typeParams.isEmpty()) return result + + // Split by comma, but respect nested angle brackets + List params = splitGenericParams(typeParams) + + for (String param in params) { + param = param.trim() + TypeParamInfo info = new TypeParamInfo() + + // Check for "T extends BoundType" pattern + def extendsMatcher = param =~ /(\w+)\s+extends\s+(.+)/ + if (extendsMatcher.find()) { + info.name = extendsMatcher.group(1).trim() + String boundType = extendsMatcher.group(2).trim() + + // Handle generic bounds like "Comparable" - extract base type + int angleIndex = boundType.indexOf('<') + String baseBoundType = angleIndex > 0 ? boundType.substring(0, angleIndex) : boundType + + info.boundType = baseBoundType + info.fullBoundType = resolveFullType(baseBoundType, parsed) + } else { + // Just a type param like "T" with no explicit bound + info.name = param + info.boundType = null + info.fullBoundType = null + } + + result << info + } + + return result + } + + private String calculateRelativePath(String fromPath, String toPath) { + // Calculate relative path between two .d.ts files + String[] fromParts = fromPath.split('/') + String[] toParts = toPath.split('/') + + // Find common prefix + int common = 0 + while (common < fromParts.length - 1 && common < toParts.length && fromParts[common] == toParts[common]) { + common++ + } + + // Build relative path + StringBuilder result = new StringBuilder() + + // Go up from current location + int ups = fromParts.length - common - 1 + if (ups == 0) { + result.append('./') + } else { + for (int i = 0; i < ups; i++) { + result.append('../') + } + } + + // Go down to target + for (int i = common; i < toParts.length; i++) { + if (i > common) result.append('/') + result.append(toParts[i]) + } + + // Remove .d.ts extension for imports + String path = result.toString() + if (path.endsWith('.d.ts')) { + path = path.substring(0, path.length() - 5) + } + + return path + } + + /** + * Convert JavaDoc to JSDoc format + */ + private String convertJsDoc(String jsdoc, String indent) { + if (jsdoc == null) return '' + + // Already in JSDoc format, just fix indentation + String[] lines = jsdoc.split('\n') + return lines.collect { line -> + String trimmed = line.trim() + if (trimmed.startsWith('*')) { + return "${indent} ${trimmed}" + } else if (trimmed.startsWith('/**') || trimmed.startsWith('*/')) { + return "${indent}${trimmed}" + } else { + return "${indent}${trimmed}" + } + }.join('\n') + } + + /** + * 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 + */ + 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')|| + 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 + ) + } + } + } + } + } + + private String deriveHookName(String eventName) { + // Convert event names to hook function names + // e.g., "InitEvent" -> "init", "DamagedEvent" -> "damaged" + String name = eventName + if (name.endsWith('Event') && name.length() > 5) { + name = name.substring(0, name.length() - 5) + } + // Convert to camelCase + if (name.isEmpty()) return 'event' + String hookName = name.length() > 1 ? (name.substring(0, 1).toLowerCase() + name.substring(1)) : name.toLowerCase() + + // Handle JavaScript reserved words + Set reservedWords = ['break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', + 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', + 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', + 'void', 'while', 'with', 'class', 'const', 'enum', 'export', 'extends', + 'import', 'super', 'implements', 'interface', 'let', 'package', 'private', + 'protected', 'public', 'static', 'yield'] as Set + + if (reservedWords.contains(hookName)) { + // Prefix with 'on' for reserved words + hookName = 'on' + name + } + + return hookName + } + + /** + * Generate index.d.ts with all type aliases + */ + private void generateIndexFile() { + StringBuilder sb = new StringBuilder() + + sb.append('/**\n') + sb.append(' * Centralized global declarations for CustomNPC+ scripting.\n') + sb.append(' * Auto-generated - do not edit manually.\n') + sb.append(' */\n\n') + + sb.append('declare global {\n') + sb.append(' // ============================================================================\n') + sb.append(' // TYPE ALIASES - Make all interfaces available globally\n') + sb.append(' // ============================================================================\n\n') + + generatedTypes.sort { a, b -> a.name <=> b.name } + + generatedTypes.each { type -> + if (!type.name.contains('.')) { // Skip nested types here + sb.append(" type ${type.name} = import('./${type.filePath.replace('.d.ts', '')}').${type.name};\n") + } + } + + // Collect all parent types that have nested types (for namespace declarations) + Map> parentToNested = [:] + generatedTypes.each { type -> + if (type.parentType) { + if (!parentToNested.containsKey(type.parentType)) { + parentToNested[type.parentType] = [] + } + parentToNested[type.parentType] << type + } + } + + // Generate namespace declarations for types with nested interfaces + if (!parentToNested.isEmpty()) { + sb.append('\n // ============================================================================\n') + sb.append(' // NESTED INTERFACES - Allow autocomplete like INpcEvent.InitEvent\n') + sb.append(' // ============================================================================\n\n') + + parentToNested.sort { a, b -> a.key <=> b.key }.each { parentName, nestedTypes -> + sb.append(" namespace ${parentName} {\n") + nestedTypes.sort { a, b -> a.name <=> b.name }.each { nested -> + // Extract just the nested type name (e.g., "InitEvent" from "IPlayerEvent.InitEvent") + String nestedName = nested.name.contains('.') ? + nested.name.substring(nested.name.lastIndexOf('.') + 1) : nested.name + sb.append(" interface ${nestedName} extends ${parentName} {}\n") + } + sb.append(" }\n\n") + } + } + + sb.append('}\n\n') + sb.append('export {};\n') + + new File(outputDir, 'index.d.ts').text = sb.toString() + } + + /** + * Generate hooks.d.ts with event hook function declarations + */ + 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(' */\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') + + hooks.each { hookName, hookInfos -> + hookInfos.each { info -> + sb.append(" function ${hookName}(${info.eventType}: ${info.fullType}): void;\n") + } + } + + sb.append('}\n\n') + sb.append('export {};\n') + + new File(outputDir, 'hooks.d.ts').text = sb.toString() + } + + // Data classes + static class ParsedJavaFile { + String packageName = '' + List imports = [] + List types = [] + } + + static class JavaType { + String name + String typeParams // Raw string like "T extends EntityPlayerMP" + List parsedTypeParams = [] // Parsed type parameters with full class names + String extendsType + List implementsTypes = [] + boolean isInterface + boolean isClass + String jsdoc + List methods = [] + List fields = [] + List nestedTypes = [] + } + + static class JavaMethod { + String name + String returnType + List parameters = [] + String jsdoc + } + + static class JavaParameter { + String name + String type + boolean isVarargs + } + + static class JavaField { + String name + String type + String jsdoc + } + + static class TypeParamInfo { + String name // e.g., "T" + String boundType // e.g., "EntityPlayerMP" + String fullBoundType // e.g., "net.minecraft.entity.player.EntityPlayerMP" + } + + static class TypeInfo { + String name + String packageName + String filePath + boolean isClass + boolean isInterface + String extendsType + String parentType + } + + static class HookInfo { + String eventType + String subEvent + String fullType + String packageName + } +} diff --git a/gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy b/gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy new file mode 100644 index 000000000..3165e3744 --- /dev/null +++ b/gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy @@ -0,0 +1,17 @@ +package dts + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * Gradle plugin for generating TypeScript definition files from Java API sources. + */ +class TypeScriptGeneratorPlugin implements Plugin { + + @Override + void apply(Project project) { + if (project.tasks.findByName('generateTypeScriptDefinitions') == null) { + project.tasks.register('generateTypeScriptDefinitions', GenerateTypeScriptTask) + } + } +} \ No newline at end of file diff --git a/gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties b/gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties new file mode 100644 index 000000000..f27cbecea --- /dev/null +++ b/gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties @@ -0,0 +1 @@ +implementation-class=dts.TypeScriptGeneratorPlugin diff --git a/settings.gradle b/settings.gradle index 791ed6664..e58786789 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,12 +1,12 @@ pluginManagement { - resolutionStrategy { - eachPlugin { - if(requested.id.toString() == "dts.typescript-generator") { - useModule("com.github.bigguy345:dts-gradle-plugin:95b8ab4") - } - } - } - +// resolutionStrategy { +// eachPlugin { +// if(requested.id.toString() == "dts.typescript-generator") { +// useModule("com.github.bigguy345:dts-gradle-plugin:95b8ab4") +// } +// } +// } +// repositories { maven { url "https://jitpack.io"} maven { @@ -27,3 +27,6 @@ pluginManagement { plugins { id 'com.gtnewhorizons.gtnhsettingsconvention' version '1.0.27' } + +// Include the local gradle-plugins composite build so plugin is resolved locally +includeBuild 'gradle-plugins' \ No newline at end of file From 55ae32545834d11393486f4341c306e814076233 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 04:09:50 +0200 Subject: [PATCH 259/337] Removed duplicate class --- .../script/autocomplete/AutocompleteItem.java | 4 +- .../type/SyntheticTypeBuilder.java | 417 ------------------ 2 files changed, 2 insertions(+), 419 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index f4e761bc4..4dcfa90e2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -365,7 +365,7 @@ public static AutocompleteItem fromSyntheticMethod( insertText.toString(), Kind.METHOD, simpleReturnType, - null, // TypeInfo for return type - would need to resolve from TypeResolver + method.getReturnTypeInfo(), // TypeInfo for return type - would need to resolve from TypeResolver method.getSignature(), method.documentation, method, @@ -396,7 +396,7 @@ public static AutocompleteItem fromSyntheticField( field.name, Kind.FIELD, field.typeName, - null, // TypeInfo for field type - would need to resolve from TypeResolver + field.getTypeInfo(), // TypeInfo for field type - would need to resolve from TypeResolver field.typeName + " " + field.name, field.documentation, field, diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java deleted file mode 100644 index 215b28bcd..000000000 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/SyntheticTypeBuilder.java +++ /dev/null @@ -1,417 +0,0 @@ -package noppes.npcs.client.gui.util.script.interpreter.type; - -import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; -import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; -import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; -import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; - -import java.lang.reflect.Modifier; -import java.util.*; - -/** - * Builder for creating synthetic (non-reflection-based) types. - * Used for built-in Nashorn objects like 'Java' that have no corresponding Java class. - * - *

Usage:

- *
- * SyntheticType javaType = new SyntheticTypeBuilder("Java")
- *     .addMethod("type")
- *         .parameter("className", "string")
- *         .returns("Class")
- *         .documentation("Loads a Java class by fully-qualified name.")
- *         .done()
- *     .build();
- * 
- */ -public class SyntheticTypeBuilder { - - private final String name; - private String documentation; - private final List methods = new ArrayList<>(); - private final List fields = new ArrayList<>(); - - public SyntheticTypeBuilder(String name) { - this.name = name; - } - - public SyntheticTypeBuilder documentation(String doc) { - this.documentation = doc; - return this; - } - - /** - * Start building a method for this type. - */ - public MethodBuilder addMethod(String methodName) { - return new MethodBuilder(this, methodName); - } - - /** - * Add a field to this type. - */ - public SyntheticTypeBuilder addField(String fieldName, String typeName, String doc) { - fields.add(new SyntheticField(fieldName, typeName, doc, false)); - return this; - } - - /** - * Add a static field to this type. - */ - public SyntheticTypeBuilder addStaticField(String fieldName, String typeName, String doc) { - fields.add(new SyntheticField(fieldName, typeName, doc, true)); - return this; - } - - void addBuiltMethod(SyntheticMethod method) { - methods.add(method); - } - - /** - * Build the synthetic type. - */ - public SyntheticType build() { - return new SyntheticType(name, documentation, methods, fields); - } - - // ==================== Inner classes ==================== - - /** - * Builder for methods within a synthetic type. - */ - public static class MethodBuilder { - private final SyntheticTypeBuilder parent; - private final String name; - private final List parameters = new ArrayList<>(); - private String returnType = "void"; - private String documentation; - private boolean isStatic = false; - private ReturnTypeResolver returnTypeResolver; - - MethodBuilder(SyntheticTypeBuilder parent, String name) { - this.parent = parent; - this.name = name; - } - - /** - * Add a parameter to this method. - */ - public MethodBuilder parameter(String paramName, String typeName) { - parameters.add(new SyntheticParameter(paramName, typeName, null)); - return this; - } - - /** - * Add a parameter with documentation. - */ - public MethodBuilder parameter(String paramName, String typeName, String doc) { - parameters.add(new SyntheticParameter(paramName, typeName, doc)); - return this; - } - - /** - * Set the return type. - */ - public MethodBuilder returns(String typeName) { - this.returnType = typeName; - return this; - } - - /** - * Set a dynamic return type resolver. - * Used for methods like Java.type() where return type depends on arguments. - */ - public MethodBuilder returnsResolved(ReturnTypeResolver resolver) { - this.returnTypeResolver = resolver; - return this; - } - - /** - * Set method documentation. - */ - public MethodBuilder documentation(String doc) { - this.documentation = doc; - return this; - } - - /** - * Mark this method as static. - */ - public MethodBuilder asStatic() { - this.isStatic = true; - return this; - } - - /** - * Finish building this method and return to the type builder. - */ - public SyntheticTypeBuilder done() { - parent.addBuiltMethod(new SyntheticMethod(name, returnType, parameters, documentation, isStatic, returnTypeResolver)); - return parent; - } - } - - /** - * Functional interface for resolving return types dynamically. - */ - @FunctionalInterface - public interface ReturnTypeResolver { - /** - * Resolve the return type given the arguments passed to the method. - * @param arguments The string arguments (for methods like Java.type("className")) - * @return The resolved TypeInfo, or null if cannot be resolved - */ - TypeInfo resolve(String[] arguments); - } - - // ==================== Data classes ==================== - - public static class SyntheticMethod { - public final String name; - public final String returnType; - public final List parameters; - public final String documentation; - public final boolean isStatic; - public final ReturnTypeResolver returnTypeResolver; - - SyntheticMethod(String name, String returnType, List parameters, - String documentation, boolean isStatic, ReturnTypeResolver returnTypeResolver) { - this.name = name; - this.returnType = returnType; - this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters)); - this.documentation = documentation; - this.isStatic = isStatic; - this.returnTypeResolver = returnTypeResolver; - } - - /** - * Create a MethodInfo from this synthetic method. - */ - public MethodInfo toMethodInfo(TypeInfo containingType) { - List paramInfos = new ArrayList<>(); - for (SyntheticParameter param : parameters) { - TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); - if (paramType == null) { - paramType = TypeInfo.unresolved(param.typeName, param.typeName); - } - paramInfos.add(FieldInfo.parameter(param.name, paramType, -1, null)); - } - - TypeInfo returnTypeInfo = TypeResolver.getInstance().resolve(returnType); - if (returnTypeInfo == null) { - returnTypeInfo = TypeInfo.unresolved(returnType, returnType); - } - - int modifiers = Modifier.PUBLIC; - if (isStatic) { - modifiers |= Modifier.STATIC; - } - - MethodInfo methodInfo = MethodInfo.external(name, returnTypeInfo, containingType, paramInfos, modifiers, null); - - // Create JSDocInfo from documentation - if (documentation != null && !documentation.isEmpty()) { - JSDocInfo jsDocInfo = createJSDocInfo(returnTypeInfo); - methodInfo.setJSDocInfo(jsDocInfo); - } - - return methodInfo; - } - - /** - * Create JSDocInfo from the documentation string. - */ - private JSDocInfo createJSDocInfo(TypeInfo returnTypeInfo) { - // Parse the documentation to extract description and separate sections - String[] lines = documentation.split("\n"); // Split on actual newlines, not \\n - StringBuilder descBuilder = new StringBuilder(); - - // Extract description (everything before @param or @returns) - for (String line : lines) { - line = line.trim(); - if (line.startsWith("@param") || line.startsWith("@returns") || line.startsWith("@return")) { - break; - } - if (!line.isEmpty()) { - if (descBuilder.length() > 0) descBuilder.append("\n"); // Use actual newline - descBuilder.append(line); - } - } - - JSDocInfo jsDocInfo = new JSDocInfo(documentation, -1, -1); - jsDocInfo.setDescription(descBuilder.toString()); - - // Add @param tags for each parameter - for (int i = 0; i < parameters.size(); i++) { - SyntheticParameter param = parameters.get(i); - TypeInfo paramType = TypeResolver.getInstance().resolve(param.typeName); - if (paramType == null) { - paramType = TypeInfo.unresolved(param.typeName, param.typeName); - } - - JSDocParamTag paramTag = JSDocParamTag.create( - -1, -1, -1, // offsets - param.typeName, paramType, -1, -1, // type info - param.name, -1, -1, // param name - param.documentation // description - ); - jsDocInfo.addParamTag(paramTag); - } - - // Add @returns tag - use simple name for display but full TypeInfo for resolution - String displayTypeName = returnTypeInfo != null && returnTypeInfo.isResolved() - ? returnTypeInfo.getSimpleName() - : returnType; - - JSDocReturnTag returnTag = JSDocReturnTag.create( - "returns", -1, -1, -1, // offsets - displayTypeName, returnTypeInfo, -1, -1, // type name for display, TypeInfo for resolution - null // description extracted from main documentation - ); - jsDocInfo.setReturnTag(returnTag); - - return jsDocInfo; - } - - /** - * Get signature string for display. - */ - public String getSignature() { - StringBuilder sb = new StringBuilder(); - sb.append(name).append("("); - for (int i = 0; i < parameters.size(); i++) { - if (i > 0) sb.append(", "); - SyntheticParameter p = parameters.get(i); - sb.append(p.name).append(": ").append(p.typeName); - } - sb.append("): ").append(returnType); - return sb.toString(); - } - } - - public static class SyntheticParameter { - public final String name; - public final String typeName; - public final String documentation; - - SyntheticParameter(String name, String typeName, String documentation) { - this.name = name; - this.typeName = typeName; - this.documentation = documentation; - } - } - - public static class SyntheticField { - public final String name; - public final String typeName; - public final String documentation; - public final boolean isStatic; - - SyntheticField(String name, String typeName, String documentation, boolean isStatic) { - this.name = name; - this.typeName = typeName; - this.documentation = documentation; - this.isStatic = isStatic; - } - - public FieldInfo toFieldInfo() { - TypeInfo type = TypeResolver.getInstance().resolve(typeName); - if (type == null) { - type = TypeInfo.unresolved(typeName, typeName); - } - int modifiers = Modifier.PUBLIC; - if (isStatic) { - modifiers |= Modifier.STATIC; - } - return FieldInfo.external(name, type, documentation, modifiers); - } - } - - // ==================== SyntheticType (the result) ==================== - - /** - * Represents a fully-built synthetic type. - */ - public static class SyntheticType { - private final String name; - private final String documentation; - private final Map methods; - private final Map fields; - private TypeInfo typeInfo; - - SyntheticType(String name, String documentation, List methods, List fields) { - this.name = name; - this.documentation = documentation; - this.methods = new LinkedHashMap<>(); - for (SyntheticMethod m : methods) { - this.methods.put(m.name, m); - } - this.fields = new LinkedHashMap<>(); - for (SyntheticField f : fields) { - this.fields.put(f.name, f); - } - } - - public String getName() { return name; } - public String getDocumentation() { return documentation; } - - public SyntheticMethod getMethod(String methodName) { - return methods.get(methodName); - } - - public Collection getMethods() { - return methods.values(); - } - - public SyntheticField getField(String fieldName) { - return fields.get(fieldName); - } - - public Collection getFields() { - return fields.values(); - } - - public boolean hasMethod(String methodName) { - return methods.containsKey(methodName); - } - - public boolean hasField(String fieldName) { - return fields.containsKey(fieldName); - } - - /** - * Get or create the TypeInfo for this synthetic type. - */ - public TypeInfo getTypeInfo() { - if (typeInfo == null) { - typeInfo = TypeInfo.resolved(name, name, "", TypeInfo.Kind.CLASS, null); - } - return typeInfo; - } - - /** - * Create a MethodInfo for a method in this type. - */ - public MethodInfo getMethodInfo(String methodName) { - SyntheticMethod method = methods.get(methodName); - if (method == null) return null; - return method.toMethodInfo(getTypeInfo()); - } - - /** - * Resolve the return type of a method given arguments. - * Used for special methods like Java.type(). - */ - public TypeInfo resolveMethodReturnType(String methodName, String[] arguments) { - SyntheticMethod method = methods.get(methodName); - if (method == null) return null; - - if (method.returnTypeResolver != null) { - return method.returnTypeResolver.resolve(arguments); - } - - // Fall back to static return type - return TypeResolver.getInstance().resolve(method.returnType); - } - } -} From 289c78f4de40eb54129ebd95b36c0a3b41316465 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 04:34:24 +0200 Subject: [PATCH 260/337] Proper TypeInfo getter for synthetic type members --- .../script/interpreter/ScriptDocument.java | 8 +++---- .../type/synthetic/SyntheticField.java | 12 ++++++++++ .../type/synthetic/SyntheticMethod.java | 24 +++++++++++++++++++ .../type/synthetic/SyntheticParameter.java | 15 ++++++++++++ .../type/synthetic/SyntheticType.java | 13 ++-------- 5 files changed, 57 insertions(+), 15 deletions(-) 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 41378ea25..0765d9b06 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 @@ -2677,10 +2677,10 @@ private void markMethodCalls(List marks) { // Check for dynamic return type resolution (like Java.type("className")) SyntheticMethod synMethod = syntheticType.getMethod(methodName); TypeInfo dynamicReturnType = null; - if (synMethod != null && synMethod.returnTypeResolver != null) { + if (synMethod != null) { String argsText = text.substring(openParen + 1, closeParen); String[] strArgs = TypeResolver.parseStringArguments(argsText); - dynamicReturnType = synMethod.returnTypeResolver.resolve(strArgs); + dynamicReturnType = synMethod.resolveReturnType(strArgs); } MethodCallInfo callInfo = new MethodCallInfo(methodName, nameStart, nameEnd, openParen, @@ -3553,9 +3553,9 @@ private TypeInfo resolveSyntheticChainSegment(SyntheticType syntheticType, Chain if (method != null) { // For methods with dynamic return type resolvers (like Java.type), // extract string arguments and resolve - if (method.returnTypeResolver != null && segment.arguments != null) { + if (segment.arguments != null) { String[] args = TypeResolver.parseStringArguments(segment.arguments); - TypeInfo resolved = method.returnTypeResolver.resolve(args); + TypeInfo resolved = method.resolveReturnType(args); if (resolved != null) { return resolved; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java index 1f798e4f6..4bb2bc684 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticField.java @@ -19,6 +19,18 @@ public class SyntheticField { this.isStatic = isStatic; } + /** + * Get the field type as TypeInfo. + * @return The resolved TypeInfo for the field type, or unresolved if not found + */ + public TypeInfo getTypeInfo() { + TypeInfo type = TypeResolver.getInstance().resolve(typeName); + if (type == null) { + type = TypeInfo.unresolved(typeName, typeName); + } + return type; + } + public FieldInfo toFieldInfo() { TypeInfo type = TypeResolver.getInstance().resolve(typeName); if (type == null) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java index 6bd554904..acae6098e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticMethod.java @@ -32,6 +32,30 @@ public class SyntheticMethod { this.returnTypeResolver = returnTypeResolver; } + /** + * Get the return type as TypeInfo. + * @return The resolved TypeInfo for the return type, or unresolved if not found + */ + public TypeInfo getReturnTypeInfo() { + TypeInfo returnTypeInfo = TypeResolver.getInstance().resolve(returnType); + if (returnTypeInfo == null) { + returnTypeInfo = TypeInfo.unresolved(returnType, returnType); + } + return returnTypeInfo; + } + + /** + * Resolve the return type with dynamic arguments (for methods like Java.type). + * @param arguments The method call arguments + * @return The dynamically resolved TypeInfo, or static return type if no resolver + */ + public TypeInfo resolveReturnType(String[] arguments) { + if (returnTypeResolver != null) { + return returnTypeResolver.resolve(arguments); + } + return getReturnTypeInfo(); + } + /** * Create a MethodInfo from this synthetic method. */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java index 9b58bebf3..bf9a5fe97 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticParameter.java @@ -1,5 +1,8 @@ package noppes.npcs.client.gui.util.script.interpreter.type.synthetic; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + public class SyntheticParameter { public final String name; public final String typeName; @@ -10,4 +13,16 @@ public class SyntheticParameter { this.typeName = typeName; this.documentation = documentation; } + + /** + * Get the parameter type as TypeInfo. + * @return The resolved TypeInfo for the parameter type, or unresolved if not found + */ + public TypeInfo getTypeInfo() { + TypeInfo type = TypeResolver.getInstance().resolve(typeName); + if (type == null) { + type = TypeInfo.unresolved(typeName, typeName); + } + return type; + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java index 356891caf..01878d2f5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/synthetic/SyntheticType.java @@ -2,12 +2,8 @@ import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Represents a fully-built synthetic type. @@ -94,11 +90,6 @@ public TypeInfo resolveMethodReturnType(String methodName, String[] arguments) { if (method == null) return null; - if (method.returnTypeResolver != null) { - return method.returnTypeResolver.resolve(arguments); - } - - // Fall back to static return type - return TypeResolver.getInstance().resolve(method.returnType); + return method.resolveReturnType(arguments); } } From a106343a0ea9ed07c35055c994041d48d15c9671 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 05:07:48 +0200 Subject: [PATCH 261/337] NashornBuiltins fixes --- .../util/script/interpreter/type/NashornBuiltins.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java index 3b425b5bb..962b5b11a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/NashornBuiltins.java @@ -90,7 +90,7 @@ private void initializeBuiltins() { .addMethod("from") .parameter("javaArray", "Object", "A Java array or Collection") - .returns("Array") + .returns("Object[]") .documentation("Converts a Java array or Collection to a JavaScript array.\n\n" + "**Usage:**\n" + "```javascript\n" + @@ -102,8 +102,8 @@ private void initializeBuiltins() { .done() .addMethod("to") - .parameter("jsArray", "Array", "A JavaScript array") - .parameter("javaType", "java.lang.Class", "The target Java array type") + .parameter("jsArray", "Object[]", "A JavaScript array") + .parameter("javaType", "Class", "The target Java array type") .returns("Object") .documentation("Converts a JavaScript array to a Java array of the specified type.\n\n" + "**Usage:**\n" + @@ -134,9 +134,9 @@ private void initializeBuiltins() { .done() .addMethod("synchronized") - .parameter("func", "Function", "The function to synchronize") + .parameter("func", "java.util.function.Function", "The function to synchronize") .parameter("lock", "Object", "The object to synchronize on") - .returns("Function") + .returns("java.util.function.Function") .documentation("Wraps a function to execute synchronized on a given object.\n\n" + "**Usage:**\n" + "```javascript\n" + From a5c02e6c944ad049284828069f529fd2f6166968 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 05:44:36 +0200 Subject: [PATCH 262/337] Fixed static access problem for AutocompleteProvider & generally --- .../util/script/interpreter/ScriptDocument.java | 2 +- .../script/interpreter/type/TypeResolver.java | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) 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 0765d9b06..a9f53957b 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 @@ -3583,7 +3583,7 @@ private TypeInfo resolveSyntheticChainSegment(SyntheticType syntheticType, Chain * This is a quick heuristic check - it may have false positives for operators * inside strings, but those are handled by the full parser. */ - private boolean containsOperators(String expr) { + public boolean containsOperators(String expr) { if (expr == null) return false; boolean inString = false; 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 7b2673a05..2ef61c838 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 @@ -718,15 +718,25 @@ public static boolean isStaticAccessExpression(String identifier, int position, return false; } - // First check if it's a variable/field (instance access) + // If the identifier contains dots, parentheses, brackets, or operators, it's a complex expression + // Complex expressions (method calls, field chains, arithmetic) are instance access by default + if (identifier.contains(".") || identifier.contains("(") || identifier.contains("[") || + document.containsOperators(identifier)) { + // For complex expressions, resolve as expression and check the result type + TypeInfo exprType = document.resolveExpressionType(identifier, position); + // Only static if the expression result is itself a ClassTypeInfo (e.g., Java.type("File")) + return isStaticAccess(exprType, false); + } + + // Simple identifier - check if it's a variable/field (instance access) FieldInfo fieldInfo = document.resolveVariable(identifier, position); if (fieldInfo != null && fieldInfo.isResolved()) { // It's a variable - check if its type is a ClassTypeInfo return isStaticAccess(fieldInfo.getTypeInfo(), false); } - // Not a variable - check if it's a type name - TypeInfo typeCheck = document.resolveExpressionType(identifier,position); + // Not a variable - check if it's a type name by trying to resolve as type + TypeInfo typeCheck = document.resolveType(identifier); boolean isType = typeCheck != null && typeCheck.isResolved(); return isStaticAccess(typeCheck, isType); } From 71050da341aeb0b625ca9d83e7cec6ee04995137 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 05:52:49 +0200 Subject: [PATCH 263/337] Fixed findReceiverExpr breaking when contains strings --- .../gui/util/script/autocomplete/AutocompleteManager.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 60f352743..1996f2cec 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -686,7 +686,12 @@ private String findReceiverExpression(String text, int dotPos) { break; } } else { - break; + // Within parentheses or brackets, allow any character (commas, operators, etc.) + if (parenDepth > 0 || bracketDepth > 0) { + start--; + } else { + break; + } } } From 78f7fa65f10e9dc8f5e85c96efa0ddba1667464d Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 9 Jan 2026 22:59:13 -0500 Subject: [PATCH 264/337] 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.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 void initGui() { 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 @@ 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 void setGuiData(NBTTagCompound compound) { 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.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 @@ private void initScriptEditorTab(int yoffset) { // 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 void setGuiData(NBTTagCompound compound) { 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.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 void setGuiData(NBTTagCompound compound) { } } + @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 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 void setGuiData(NBTTagCompound compound) { } } + @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.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 String getLanguage() { 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.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 boolean isJavaScript() { 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 @@ private void parseMethodDeclarations() { 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.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 String getLanguage() { 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 void init(String text, int width, int height) { } 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 void registerTypeAlias(String alias, String fullType) { } /** - * 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 String getHookParameterType(String functionName) { } 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 void clear() { 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 void parseDefinitionFile(String content, String fileName) { /** * 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 List getJSHookSignatures(String functionNam 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(); + } +} From 576401ebde94870755da3c8e33574b3b9148ee18 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 06:31:27 +0200 Subject: [PATCH 265/337] Added object instantiation for Jave.type: i.e. var FileClass = Java.type("java.io.File") var file = new FileClass("name") --- .../script/interpreter/ScriptDocument.java | 125 ++++++++++++++++-- 1 file changed, 117 insertions(+), 8 deletions(-) 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 5ead47766..d0be41eee 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 @@ -1295,8 +1295,29 @@ private TypeInfo inferChainType(String firstIdent, int identStart, int dotPositi String argsText = text.substring(pos + 1, closeParen).trim(); TypeInfo[] argTypes = parseArgumentTypes(argsText, pos + 1); - MethodInfo methodInfo = currentType.getBestMethodOverload(segment, argTypes); - currentType = (methodInfo != null) ? methodInfo.getReturnType() : null; + // Check if this is a synthetic type with dynamic return type resolver (like Java.type()) + SyntheticType syntheticType = null; + if (isJavaScript() && typeResolver.isSyntheticType(firstIdent)) { + syntheticType = typeResolver.getSyntheticType(firstIdent); + } + + if (syntheticType != null && syntheticType.hasMethod(segment)) { + SyntheticMethod synMethod = syntheticType.getMethod(segment); + if (synMethod != null) { + // Try dynamic resolution first + String[] strArgs = TypeResolver.parseStringArguments(argsText); + TypeInfo dynamicType = synMethod.resolveReturnType(strArgs); + if (dynamicType != null) { + currentType = dynamicType; + } else { + // Fall back to static return type + currentType = synMethod.getReturnTypeInfo(); + } + } + } else { + MethodInfo methodInfo = currentType.getBestMethodOverload(segment, argTypes); + currentType = (methodInfo != null) ? methodInfo.getReturnType() : null; + } // Skip to after the closing paren pos = closeParen + 1; @@ -2231,6 +2252,11 @@ private void markImplementsClause(List marks, int clauseStart, } private void markTypeDeclarations(List marks) { + // First, mark variables that hold Java.type() results + if (isJavaScript()) { + markJavaTypeVariables(marks); + } + // Pattern for type optionally followed by generics - we'll manually parse generics Pattern typeStart = Pattern.compile( "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + @@ -2315,6 +2341,37 @@ else if (c == '>') + /** + * Mark variables that hold Java.type() class references. + * Example: var File = Java.type("java.io.File"); + */ + private void markJavaTypeVariables(List marks) { + // Pattern: var/let/const varName = Java.type("className") + Pattern javaTypePattern = Pattern.compile( + "\\b(var|let|const)\\s+(\\w+)\\s*=\\s*Java\\.type\\s*\\(\\s*[\"']([^\"']+)[\"']\\s*\\)"); + + Matcher m = javaTypePattern.matcher(text); + + while (m.find()) { + if (isExcluded(m.start())) continue; + + String varName = m.group(2); + String className = m.group(3); + int varStart = m.start(2); + int varEnd = m.end(2); + + // Resolve the class name + TypeInfo classType = typeResolver.resolveFullName(className); + if (classType != null && classType.isResolved()) { + // Create ClassTypeInfo to represent that this variable holds a Class reference + ClassTypeInfo classRef = new ClassTypeInfo(classType); + + // Mark the variable with the ClassTypeInfo for hover info + marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, classRef)); + } + } + } + /** * Recursively parse and mark generic type parameters. * Handles arbitrarily nested generics like Map>>. @@ -3199,7 +3256,18 @@ public TypeInfo resolveExpressionType(String expr, int position) { if (expr.startsWith("new ")) { Matcher newMatcher = NEW_TYPE_PATTERN.matcher(expr); if (newMatcher.find()) { - return resolveType(newMatcher.group(1)); + String typeName = newMatcher.group(1); + + // First check if it's a variable holding a ClassTypeInfo (like var File = Java.type("java.io.File")) + FieldInfo varInfo = resolveVariable(typeName, position); + if (varInfo != null && varInfo.getTypeInfo() instanceof ClassTypeInfo) { + // It's a variable holding a class reference, return the wrapped class + ClassTypeInfo classRef = (ClassTypeInfo) varInfo.getTypeInfo(); + return classRef.getInstanceType(); + } + + // Otherwise treat it as a type name + return resolveType(typeName); } } @@ -3913,7 +3981,18 @@ private TypeInfo resolveSimpleExpression(String expr, int position) { if (expr.startsWith("new ")) { Matcher newMatcher = NEW_TYPE_PATTERN.matcher(expr); if (newMatcher.find()) { - return resolveType(newMatcher.group(1)); + String typeName = newMatcher.group(1); + + // First check if it's a variable holding a ClassTypeInfo (like var File = Java.type("java.io.File")) + FieldInfo varInfo = resolveVariable(typeName, position); + if (varInfo != null && varInfo.getTypeInfo() instanceof ClassTypeInfo) { + // It's a variable holding a class reference, return the wrapped class + ClassTypeInfo classRef = (ClassTypeInfo) varInfo.getTypeInfo(); + return classRef.getInstanceType(); + } + + // Otherwise treat it as a type name + return resolveType(typeName); } } @@ -5168,6 +5247,19 @@ private void markImportedClassUsages(List marks) { // Try to resolve the class TypeInfo info = resolveType(className); + boolean isVariableHoldingClass = false; + + // If not a class name, check if it's a variable holding a ClassTypeInfo + if ((info == null || !info.isResolved()) && isJavaScript()) { + FieldInfo varInfo = resolveVariable(className, start); + if (varInfo != null && varInfo.getTypeInfo() instanceof ClassTypeInfo) { + // Variable holds a class reference (like var File = Java.type("java.io.File")) + ClassTypeInfo classRef = (ClassTypeInfo) varInfo.getTypeInfo(); + info = classRef.getInstanceType(); + isVariableHoldingClass = true; + } + } + if (info != null && info.isResolved()) { boolean isNewCreation = newKeyword != null && newKeyword.trim().equals("new"); boolean isConstructorDecl = info instanceof ScriptTypeInfo && className.equals(info.getSimpleName()); @@ -5208,10 +5300,27 @@ private void markImportedClassUsages(List marks) { // Try to find matching constructor (may be null if not found) MethodInfo constructor = info.hasConstructors() ? info.findConstructor(argTypes) : null; - // Create MethodCallInfo for constructor (even if null, so errors are tracked) - MethodCallInfo ctorCall = MethodCallInfo.constructor( - info, constructor, start, end, openParen, closeParen, arguments - ); + // Create MethodCallInfo for constructor + // Use the actual variable name (className) for variables, not the class's simple name + MethodCallInfo ctorCall; + if (isVariableHoldingClass) { + // Use constructor directly with variable name + ctorCall = new MethodCallInfo( + className, // Use variable name, not class name + start, end, + openParen, closeParen, + arguments, + info, // The actual class type + constructor, + false + ).setConstructor(true); + } else { + // Use factory method for regular types + ctorCall = MethodCallInfo.constructor( + info, constructor, start, end, openParen, closeParen, arguments + ); + } + ctorCall.validate(); methodCalls.add(ctorCall); // Add to methodCalls list for error tracking marks.add(new ScriptLine.Mark(start, end, info.getTokenType(), ctorCall)); From 5b36cdac1bbef7f3dec728c481e289f7abd55928 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 06:53:48 +0200 Subject: [PATCH 266/337] Made ScriptContext class based instead --- .../noppes/npcs/constants/ScriptContext.java | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index a4567955b..942c406e6 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -1,5 +1,7 @@ package noppes.npcs.constants; +import noppes.npcs.api.event.*; + import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -30,35 +32,34 @@ public class ScriptContext { // ==================== BUILT-IN CONTEXTS ==================== public static final ScriptContext NPC = register("NPC", - "INpcEvent", - "IProjectileEvent" + INpcEvent.class, + IProjectileEvent.class ); public static final ScriptContext PLAYER = register("PLAYER", - "IPlayerEvent", - "IAnimationEvent", - "IPartyEvent", - "IDialogEvent", - "IQuestEvent", - "IFactionEvent", - "ICustomGuiEvent", - "IEffectEvent" + IPlayerEvent.class, + IAnimationEvent.class, + IPartyEvent.class, + IDialogEvent.class, + IQuestEvent.class, + IFactionEvent.class, + ICustomGuiEvent.class ); public static final ScriptContext BLOCK = register("BLOCK", - "IBlockEvent" + IBlockEvent.class ); public static final ScriptContext ITEM = register("ITEM", - "IItemEvent" + IItemEvent.class ); public static final ScriptContext FORGE = register("FORGE", - "IForgeEvent" + IForgeEvent.class ); public static final ScriptContext GLOBAL = register("GLOBAL", - "Global" + "Global" // Special case: Global namespace doesn't have a corresponding event class ); // ==================== INSTANCE FIELDS ==================== @@ -134,6 +135,25 @@ public static ScriptContext register(String id, String... namespaces) { return context; } + /** + * Register a new script context using event interface classes. + * Automatically extracts the simple name from each class. + * + * Example: + * ScriptContext.register("NPC", INpcEvent.class, IProjectileEvent.class); + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param eventClasses The event interface classes (simple names will be extracted) + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, Class... eventClasses) { + String[] namespaces = new String[eventClasses.length]; + for (int i = 0; i < eventClasses.length; i++) { + namespaces[i] = eventClasses[i].getSimpleName(); + } + return register(id, namespaces); + } + /** * Get a script context by its ID. * From 06aa0ce22e609e0e894afc97f4a0499381f255be Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 07:12:46 +0200 Subject: [PATCH 267/337] Annotations for API events --- .../noppes/npcs/constants/ScriptContext.java | 219 ++++++++++++++---- .../npcs/constants/ScriptContextMarker.java | 27 +++ 2 files changed, 203 insertions(+), 43 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/ScriptContextMarker.java diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index 942c406e6..59d02d98d 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -2,65 +2,49 @@ import noppes.npcs.api.event.*; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLDecoder; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; /** * 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. + * Script contexts are automatically discovered by scanning for {@link ScriptContextMarker} + * annotations on event interfaces. For example: * - * This is a registerable system - any mod can register their own contexts: + *
+ * {@literal @}ScriptContextMarker("NPC")
+ * public interface INpcEvent extends ICustomNPCsEvent { }
+ * 
* - * // Register a new context with multiple namespaces: - * ScriptContext.register("DBC", "IDBCEvent", "IDBCPlayerEvent", "IDBCFormEvent"); - * - * // Or add namespaces to an existing context: - * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); + * Multiple interfaces can share the same context ID - they will be grouped together. + * This is a registerable system - any mod can register their own contexts by annotating + * their event interfaces. */ public class ScriptContext { // Registry of all script contexts private static final Map REGISTRY = new ConcurrentHashMap<>(); + + // Static initializer to scan and register annotated classes + static { + scanAndRegisterAnnotatedClasses(); + } // ==================== BUILT-IN CONTEXTS ==================== + // These are loaded dynamically from annotations, but we keep references for convenience - public static final ScriptContext NPC = register("NPC", - INpcEvent.class, - IProjectileEvent.class - ); - - public static final ScriptContext PLAYER = register("PLAYER", - IPlayerEvent.class, - IAnimationEvent.class, - IPartyEvent.class, - IDialogEvent.class, - IQuestEvent.class, - IFactionEvent.class, - ICustomGuiEvent.class - ); - - public static final ScriptContext BLOCK = register("BLOCK", - IBlockEvent.class - ); - - public static final ScriptContext ITEM = register("ITEM", - IItemEvent.class - ); - - public static final ScriptContext FORGE = register("FORGE", - IForgeEvent.class - ); - - public static final ScriptContext GLOBAL = register("GLOBAL", - "Global" // Special case: Global namespace doesn't have a corresponding event class - ); + public static final ScriptContext NPC = byId("NPC"); + public static final ScriptContext PLAYER = byId("PLAYER"); + public static final ScriptContext BLOCK = byId("BLOCK"); + public static final ScriptContext ITEM = byId("ITEM"); + public static final ScriptContext FORGE = byId("FORGE"); + public static final ScriptContext GLOBAL = byId("GLOBAL"); // ==================== INSTANCE FIELDS ==================== @@ -122,6 +106,155 @@ public String getPrimaryNamespace() { // ==================== REGISTRATION API ==================== + /** + * Scan the classpath for classes annotated with {@link ScriptContextMarker} + * and automatically register them. + */ + private static void scanAndRegisterAnnotatedClasses() { + try { + // Scan the noppes.npcs.api.event package for annotated interfaces + List> annotatedClasses = findAnnotatedClasses("noppes.npcs.api.event", ScriptContextMarker.class); + + // Group classes by their context ID + Map>> contextGroups = new HashMap<>(); + + for (Class eventClass : annotatedClasses) { + ScriptContextMarker annotation = eventClass.getAnnotation(ScriptContextMarker.class); + if (annotation != null) { + String contextId = annotation.value(); + contextGroups.computeIfAbsent(contextId, k -> new ArrayList<>()).add(eventClass); + } + } + + // Register each context with its grouped classes + for (Map.Entry>> entry : contextGroups.entrySet()) { + String contextId = entry.getKey(); + List> classes = entry.getValue(); + register(contextId, classes.toArray(new Class[0])); + } + + // Special case: GLOBAL context (no annotation needed) + if (!REGISTRY.containsKey("GLOBAL")) { + register("GLOBAL", "Global"); + } + } catch (Exception e) { + // Fallback: if scanning fails, at least register known core events + System.err.println("[ScriptContext] Failed to scan for annotated classes: " + e.getMessage()); + e.printStackTrace(); + fallbackRegistration(); + } + } + + /** + * Find all classes in a package that have a specific annotation. + */ + private static List> findAnnotatedClasses(String packageName, Class annotationClass) throws Exception { + List> annotatedClasses = new ArrayList<>(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + String packagePath = packageName.replace('.', '/'); + + // Get resources for the package + Enumeration resources = classLoader.getResources(packagePath); + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + String protocol = resource.getProtocol(); + + if ("file".equals(protocol)) { + // Scanning from file system (development environment) + String filePath = URLDecoder.decode(resource.getFile(), "UTF-8"); + File directory = new File(filePath); + if (directory.exists()) { + findClassesInDirectory(directory, packageName, annotationClass, annotatedClasses); + } + } else if ("jar".equals(protocol)) { + // Scanning from JAR file (production) + String jarPath = resource.getPath().substring(5, resource.getPath().indexOf("!")); + jarPath = URLDecoder.decode(jarPath, "UTF-8"); + findClassesInJar(jarPath, packageName, annotationClass, annotatedClasses); + } + } + + return annotatedClasses; + } + + /** + * Find classes with annotation in a directory (file system). + */ + private static void findClassesInDirectory(File directory, String packageName, + Class annotationClass, + List> result) { + File[] files = directory.listFiles(); + if (files == null) return; + + for (File file : files) { + String fileName = file.getName(); + if (file.isDirectory()) { + findClassesInDirectory(file, packageName + "." + fileName, annotationClass, result); + } else if (fileName.endsWith(".class")) { + String className = packageName + '.' + fileName.substring(0, fileName.length() - 6); + try { + Class clazz = Class.forName(className); + if (clazz.isAnnotationPresent(annotationClass)) { + result.add(clazz); + } + } catch (Throwable e) { + // Skip classes that can't be loaded + } + } + } + } + + /** + * Find classes with annotation in a JAR file. + */ + private static void findClassesInJar(String jarPath, String packageName, + Class annotationClass, + List> result) { + try { + JarFile jarFile = new JarFile(jarPath); + Enumeration entries = jarFile.entries(); + String packagePath = packageName.replace('.', '/'); + + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + + if (entryName.startsWith(packagePath) && entryName.endsWith(".class")) { + String className = entryName.replace('/', '.').substring(0, entryName.length() - 6); + try { + Class clazz = Class.forName(className); + if (clazz.isAnnotationPresent(annotationClass)) { + result.add(clazz); + } + } catch (Throwable e) { + // Skip classes that can't be loaded + } + } + } + jarFile.close(); + } catch (IOException e) { + System.err.println("[ScriptContext] Failed to scan JAR: " + jarPath); + } + } + + /** + * Fallback registration if classpath scanning fails. + */ + private static void fallbackRegistration() { + try { + register("NPC", INpcEvent.class, IProjectileEvent.class); + register("PLAYER", IPlayerEvent.class, IAnimationEvent.class, IPartyEvent.class, + IDialogEvent.class, IQuestEvent.class, IFactionEvent.class, ICustomGuiEvent.class); + register("BLOCK", IBlockEvent.class); + register("ITEM", IItemEvent.class); + register("FORGE", IForgeEvent.class); + register("GLOBAL", "Global"); + } catch (Exception e) { + System.err.println("[ScriptContext] Fallback registration also failed: " + e.getMessage()); + } + } + /** * Register a new script context with one or more namespaces. * diff --git a/src/main/java/noppes/npcs/constants/ScriptContextMarker.java b/src/main/java/noppes/npcs/constants/ScriptContextMarker.java new file mode 100644 index 000000000..6da6e23ae --- /dev/null +++ b/src/main/java/noppes/npcs/constants/ScriptContextMarker.java @@ -0,0 +1,27 @@ +package noppes.npcs.constants; + +import java.lang.annotation.*; + +/** + * Marks an event interface as belonging to a specific script context. + * + * Usage: + *
+ * {@literal @}ScriptContextMarker("NPC")
+ * public interface INpcEvent extends ICustomNPCsEvent {
+ *     // ...
+ * }
+ * 
+ * + * Multiple interfaces can share the same context name - they will all be + * registered as namespaces for that context. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ScriptContextMarker { + /** + * The script context ID this event interface belongs to. + * Examples: "NPC", "PLAYER", "BLOCK", "ITEM", "FORGE", "GLOBAL" + */ + String value(); +} From c82f7517de49dd8be6ce8afa63e5545dae72be5a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 07:13:00 +0200 Subject: [PATCH 268/337] Revert "Annotations for API events" This reverts commit 06aa0ce22e609e0e894afc97f4a0499381f255be. --- .../noppes/npcs/constants/ScriptContext.java | 219 ++++-------------- .../npcs/constants/ScriptContextMarker.java | 27 --- 2 files changed, 43 insertions(+), 203 deletions(-) delete mode 100644 src/main/java/noppes/npcs/constants/ScriptContextMarker.java diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index 59d02d98d..942c406e6 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -2,49 +2,65 @@ import noppes.npcs.api.event.*; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.net.URLDecoder; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; /** * Represents a script context that maps to one or more event interface namespaces. * - * Script contexts are automatically discovered by scanning for {@link ScriptContextMarker} - * annotations on event interfaces. For example: + * 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. * - *
- * {@literal @}ScriptContextMarker("NPC")
- * public interface INpcEvent extends ICustomNPCsEvent { }
- * 
+ * This is a registerable system - any mod can register their own contexts: * - * Multiple interfaces can share the same context ID - they will be grouped together. - * This is a registerable system - any mod can register their own contexts by annotating - * their event interfaces. + * // 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<>(); - - // Static initializer to scan and register annotated classes - static { - scanAndRegisterAnnotatedClasses(); - } // ==================== BUILT-IN CONTEXTS ==================== - // These are loaded dynamically from annotations, but we keep references for convenience - public static final ScriptContext NPC = byId("NPC"); - public static final ScriptContext PLAYER = byId("PLAYER"); - public static final ScriptContext BLOCK = byId("BLOCK"); - public static final ScriptContext ITEM = byId("ITEM"); - public static final ScriptContext FORGE = byId("FORGE"); - public static final ScriptContext GLOBAL = byId("GLOBAL"); + public static final ScriptContext NPC = register("NPC", + INpcEvent.class, + IProjectileEvent.class + ); + + public static final ScriptContext PLAYER = register("PLAYER", + IPlayerEvent.class, + IAnimationEvent.class, + IPartyEvent.class, + IDialogEvent.class, + IQuestEvent.class, + IFactionEvent.class, + ICustomGuiEvent.class + ); + + public static final ScriptContext BLOCK = register("BLOCK", + IBlockEvent.class + ); + + public static final ScriptContext ITEM = register("ITEM", + IItemEvent.class + ); + + public static final ScriptContext FORGE = register("FORGE", + IForgeEvent.class + ); + + public static final ScriptContext GLOBAL = register("GLOBAL", + "Global" // Special case: Global namespace doesn't have a corresponding event class + ); // ==================== INSTANCE FIELDS ==================== @@ -106,155 +122,6 @@ public String getPrimaryNamespace() { // ==================== REGISTRATION API ==================== - /** - * Scan the classpath for classes annotated with {@link ScriptContextMarker} - * and automatically register them. - */ - private static void scanAndRegisterAnnotatedClasses() { - try { - // Scan the noppes.npcs.api.event package for annotated interfaces - List> annotatedClasses = findAnnotatedClasses("noppes.npcs.api.event", ScriptContextMarker.class); - - // Group classes by their context ID - Map>> contextGroups = new HashMap<>(); - - for (Class eventClass : annotatedClasses) { - ScriptContextMarker annotation = eventClass.getAnnotation(ScriptContextMarker.class); - if (annotation != null) { - String contextId = annotation.value(); - contextGroups.computeIfAbsent(contextId, k -> new ArrayList<>()).add(eventClass); - } - } - - // Register each context with its grouped classes - for (Map.Entry>> entry : contextGroups.entrySet()) { - String contextId = entry.getKey(); - List> classes = entry.getValue(); - register(contextId, classes.toArray(new Class[0])); - } - - // Special case: GLOBAL context (no annotation needed) - if (!REGISTRY.containsKey("GLOBAL")) { - register("GLOBAL", "Global"); - } - } catch (Exception e) { - // Fallback: if scanning fails, at least register known core events - System.err.println("[ScriptContext] Failed to scan for annotated classes: " + e.getMessage()); - e.printStackTrace(); - fallbackRegistration(); - } - } - - /** - * Find all classes in a package that have a specific annotation. - */ - private static List> findAnnotatedClasses(String packageName, Class annotationClass) throws Exception { - List> annotatedClasses = new ArrayList<>(); - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - String packagePath = packageName.replace('.', '/'); - - // Get resources for the package - Enumeration resources = classLoader.getResources(packagePath); - - while (resources.hasMoreElements()) { - URL resource = resources.nextElement(); - String protocol = resource.getProtocol(); - - if ("file".equals(protocol)) { - // Scanning from file system (development environment) - String filePath = URLDecoder.decode(resource.getFile(), "UTF-8"); - File directory = new File(filePath); - if (directory.exists()) { - findClassesInDirectory(directory, packageName, annotationClass, annotatedClasses); - } - } else if ("jar".equals(protocol)) { - // Scanning from JAR file (production) - String jarPath = resource.getPath().substring(5, resource.getPath().indexOf("!")); - jarPath = URLDecoder.decode(jarPath, "UTF-8"); - findClassesInJar(jarPath, packageName, annotationClass, annotatedClasses); - } - } - - return annotatedClasses; - } - - /** - * Find classes with annotation in a directory (file system). - */ - private static void findClassesInDirectory(File directory, String packageName, - Class annotationClass, - List> result) { - File[] files = directory.listFiles(); - if (files == null) return; - - for (File file : files) { - String fileName = file.getName(); - if (file.isDirectory()) { - findClassesInDirectory(file, packageName + "." + fileName, annotationClass, result); - } else if (fileName.endsWith(".class")) { - String className = packageName + '.' + fileName.substring(0, fileName.length() - 6); - try { - Class clazz = Class.forName(className); - if (clazz.isAnnotationPresent(annotationClass)) { - result.add(clazz); - } - } catch (Throwable e) { - // Skip classes that can't be loaded - } - } - } - } - - /** - * Find classes with annotation in a JAR file. - */ - private static void findClassesInJar(String jarPath, String packageName, - Class annotationClass, - List> result) { - try { - JarFile jarFile = new JarFile(jarPath); - Enumeration entries = jarFile.entries(); - String packagePath = packageName.replace('.', '/'); - - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - String entryName = entry.getName(); - - if (entryName.startsWith(packagePath) && entryName.endsWith(".class")) { - String className = entryName.replace('/', '.').substring(0, entryName.length() - 6); - try { - Class clazz = Class.forName(className); - if (clazz.isAnnotationPresent(annotationClass)) { - result.add(clazz); - } - } catch (Throwable e) { - // Skip classes that can't be loaded - } - } - } - jarFile.close(); - } catch (IOException e) { - System.err.println("[ScriptContext] Failed to scan JAR: " + jarPath); - } - } - - /** - * Fallback registration if classpath scanning fails. - */ - private static void fallbackRegistration() { - try { - register("NPC", INpcEvent.class, IProjectileEvent.class); - register("PLAYER", IPlayerEvent.class, IAnimationEvent.class, IPartyEvent.class, - IDialogEvent.class, IQuestEvent.class, IFactionEvent.class, ICustomGuiEvent.class); - register("BLOCK", IBlockEvent.class); - register("ITEM", IItemEvent.class); - register("FORGE", IForgeEvent.class); - register("GLOBAL", "Global"); - } catch (Exception e) { - System.err.println("[ScriptContext] Fallback registration also failed: " + e.getMessage()); - } - } - /** * Register a new script context with one or more namespaces. * diff --git a/src/main/java/noppes/npcs/constants/ScriptContextMarker.java b/src/main/java/noppes/npcs/constants/ScriptContextMarker.java deleted file mode 100644 index 6da6e23ae..000000000 --- a/src/main/java/noppes/npcs/constants/ScriptContextMarker.java +++ /dev/null @@ -1,27 +0,0 @@ -package noppes.npcs.constants; - -import java.lang.annotation.*; - -/** - * Marks an event interface as belonging to a specific script context. - * - * Usage: - *
- * {@literal @}ScriptContextMarker("NPC")
- * public interface INpcEvent extends ICustomNPCsEvent {
- *     // ...
- * }
- * 
- * - * Multiple interfaces can share the same context name - they will all be - * registered as namespaces for that context. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) -public @interface ScriptContextMarker { - /** - * The script context ID this event interface belongs to. - * Examples: "NPC", "PLAYER", "BLOCK", "ITEM", "FORGE", "GLOBAL" - */ - String value(); -} From 3ef16fb2abc6aa2907925ecaa098f9a6a1dca169 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 11 Jan 2026 17:03:00 +0200 Subject: [PATCH 269/337] submodules --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index ed5640d23..76ce22d71 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit ed5640d2385d5626a8dff2dd2d0bc59b6c455f4c +Subproject commit 76ce22d71317860d8efed5ba2d4c622bcafb95a1 From 4311931f250088a43fff2340b08e10a1fd3512c8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 12 Jan 2026 03:28:18 +0200 Subject: [PATCH 270/337] Made these 3 detached GuiScripts depend on GuiScriptInterface --- .../client/gui/script/GuiScriptEffect.java | 404 +---------------- .../client/gui/script/GuiScriptInterface.java | 179 +++++++- .../gui/script/GuiScriptLinkedItem.java | 404 +---------------- .../client/gui/script/GuiScriptRecipe.java | 414 +----------------- 4 files changed, 209 insertions(+), 1192 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index 22db14a2e..86aeec3cc 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -1,65 +1,25 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -import noppes.npcs.client.gui.util.GuiNPCInterface; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.CustomEffect; import noppes.npcs.controllers.data.EffectScript; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +public class GuiScriptEffect extends GuiScriptInterface { -public class GuiScriptEffect extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNPCManageEffects parent; public final CustomEffect effect; private final EffectScript scriptHandler; - boolean loaded = false; - - private GuiScriptTextArea textArea; - public GuiScriptEffect(GuiNPCManageEffects parent, CustomEffect effect) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; this.effect = effect; this.scriptHandler = new EffectScript(); + this.handler = this.scriptHandler; + this.singleContainer = true; for (EffectScript.ScriptType type : EffectScript.ScriptType.values()) { this.hookList.add(type.function); @@ -68,356 +28,26 @@ public GuiScriptEffect(GuiNPCManageEffects parent, CustomEffect effect) { EffectScriptPacket.Get(effect.id); } - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - else - textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); - - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; - } - - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.GLOBAL; } - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - + protected void setHandlerContainer(ScriptContainer container) { + this.scriptHandler.container = container; } + @Override public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); + setGuiDataWithOldContainer(compound); } - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - EffectScriptPacket.Save(effect.id, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - EffectScriptPacket.Save(effect.id, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + EffectScriptPacket.Save(effect.id, index, totalCount, scriptNBT); } @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; + public void save() { + saveWithPackets(); } -} +} \ No newline at end of file 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 e875e0619..527df14bd 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -1,9 +1,6 @@ package noppes.npcs.client.gui.script; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.client.gui.*; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.StatCollector; @@ -48,6 +45,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected boolean loaded = false; private Map textAreas = new HashMap<>(); + protected GuiScreen parent; + + public boolean singleContainer = false; // ==================== FULLSCREEN MODE ==================== /** Whether the editor viewport is currently in fullscreen mode */ @@ -100,20 +100,28 @@ public void initGui() { int topXoffset = 0; int topYoffset = 0; - for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { - if (ta % 20 == 0 && ta > 0) { - topYoffset -= 20; - topXoffset -= top.width + 20 * 22; + if(!singleContainer) { + for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { + if (ta % 20 == 0 && ta > 0) { + topYoffset -= 20; + topXoffset -= top.width + 20 * 22; + } + this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, + top.yPosition + topYoffset, ta + 1 + "")); + topXoffset = 0; + topYoffset = 0; + scriptLimit = ta + 2; } - this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, ta + 1 + "")); - topXoffset = 0; - topYoffset = 0; - scriptLimit = ta + 2; } - if (this.handler.getScripts().size() < 100) + if (singleContainer && getFirst() != null) + this.addTopButton( + top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, + "Script")); + else if (this.handler.getScripts().size() < 100) this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); + top = this.getTopButton(this.activeTab); if (top == null) { this.activeTab = 0; @@ -333,7 +341,7 @@ protected String getConsoleText() { return builder.toString(); } - private int getScriptIndex() { + public int getScriptIndex() { int i = 0; for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { @@ -364,7 +372,10 @@ public void confirmClicked(boolean flag, int i) { this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); } if (i == 10) { - this.handler.getScripts().remove(this.activeTab - 1); + if (singleContainer) + setHandlerContainer(null); + else + this.handler.getScripts().remove(this.activeTab - 1); this.activeTab = 0; } if (i == 101) { @@ -389,7 +400,7 @@ public void confirmClicked(boolean flag, int i) { this.displayGuiScreen(this); } - private GuiScriptTextArea getActiveScriptArea() { + public GuiScriptTextArea getActiveScriptArea() { if (this.activeTab > 0) { int idx = this.activeTab - 1; if (textAreas.containsKey(idx)) @@ -408,6 +419,11 @@ public GuiNpcTextField getTextField(int id) { return super.getTextField(id); } + public ScriptContainer getFirst() { + if (this.handler.getScripts().isEmpty()) + return null; + return this.handler.getScripts().get(0); + } @Override protected void actionPerformed(GuiButton guibutton) { if (guibutton.id >= 0 && guibutton.id < scriptLimit) { @@ -417,8 +433,16 @@ protected void actionPerformed(GuiButton guibutton) { } if (guibutton.id == scriptLimit) { - this.handler.getScripts().add(new ScriptContainer(this.handler)); - this.activeTab = this.handler.getScripts().size(); + if (singleContainer) { + if (getFirst() == null) + setHandlerContainer(new ScriptContainer(handler)); + else + this.setScript(); + this.activeTab = 1; + } else { + this.handler.getScripts().add(new ScriptContainer(this.handler)); + this.activeTab = this.handler.getScripts().size(); + } this.initGui(); } @@ -475,18 +499,26 @@ protected void actionPerformed(GuiButton guibutton) { if (guibutton.id == 107) { container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); if (container == null) { - this.handler.getScripts().add(container = new ScriptContainer(this.handler)); + container = new ScriptContainer(this.handler); + if (singleContainer) + setHandlerContainer(container); + else + this.handler.getScripts().add(container); } this.setSubGui(new EventGuiScriptList((List) this.languages.get(this.handler.getLanguage()), container)); } } - private void setScript() { + protected void setScript() { if (this.activeTab > 0) { ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); if (container == null) { - this.handler.getScripts().add(container = new ScriptContainer(this.handler)); + container = new ScriptContainer(this.handler); + if (singleContainer) + setHandlerContainer(container); + else + this.handler.getScripts().add(container); } String text = (this.getTextField(2)).getText(); @@ -527,6 +559,96 @@ protected ScriptContext getScriptContext() { return ScriptContext.GLOBAL; } + // ==================== UNIFIED SCRIPT DATA HANDLING ==================== + + /** + * Unified setGuiData for script GUIs with old container system. + * Handles both language data and tab-specific script loading. + */ + protected void setGuiDataWithOldContainer(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + loaded = true; + return; + } + + if (!compound.hasKey("Tab")) { + this.handler.setLanguage(compound.getString("ScriptLanguage")); + this.handler.setEnabled(compound.getBoolean("ScriptEnabled")); + this.loadLanguagesData(compound); + } else { + int tab = compound.getInteger("Tab"); + ScriptContainer container = new ScriptContainer(this.handler); + container.readFromNBT(compound.getCompoundTag("Script")); + this.setHandlerContainer(container); + this.initGui(); + } + } + + /** + * Load languages data from NBT. + * Separated for potential override in subclasses if needed. + */ + protected void loadLanguagesData(NBTTagCompound compound) { + NBTTagList data = compound.getTagList("Languages", 10); + HashMap languages = new HashMap(); + + for (int i = 0; i < data.tagCount(); ++i) { + NBTTagCompound comp = data.getCompoundTagAt(i); + java.util.ArrayList scripts = new java.util.ArrayList(); + NBTTagList list = comp.getTagList("Scripts", 8); + + for (int j = 0; j < list.tagCount(); ++j) { + scripts.add(list.getStringTagAt(j)); + } + + languages.put(comp.getString("Language"), scripts); + } + + this.languages = languages; + this.initGui(); + } + + /** + * Set the handler's container. Override if handler is not IScriptHandler. + */ + protected void setHandlerContainer(ScriptContainer container) { + // Default implementation - subclasses may need to cast and set differently + // e.g., ((LinkedItemScript) handler).container = container; + } + + /** + * Unified save method for script GUIs with packet-based saving. + * Subclasses only need to override sendScriptPackets() and sendMetadataPacket(). + */ + protected void saveWithPackets() { + if (loaded) { + this.setScript(); + + List containers = this.handler.getScripts(); + for (int i = 0; i < containers.size(); i++) { + ScriptContainer container = containers.get(i); + sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); + } + + NBTTagCompound scriptData = new NBTTagCompound(); + scriptData.setString("ScriptLanguage", this.handler.getLanguage()); + scriptData.setBoolean("ScriptEnabled", this.handler.getEnabled()); + scriptData.setTag("ScriptConsole", noppes.npcs.NBTTags.NBTLongStringMap(this.handler.getConsoleText())); + + sendSavePacket(-1, containers.size(), scriptData); + } + } + + /** + * Send a script container packet. Override in subclasses. + * @param index The index of this script + * @param totalCount Total number of scripts + * @param scriptNBT The script container NBT data + */ + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + // Default: do nothing (for non-packet-based GUIs like GuiScriptItem) + } + public void save() { if (loaded) this.setScript(); @@ -540,14 +662,27 @@ public void textUpdate(String text) { } + @Deprecated @Override + //NEVER USED public void saveText(String text) { - ScriptContainer container = handler.getScripts().get(activeTab); + ScriptContainer container = handler.getScripts().get(activeTab ); //activeTab - 1? if (container != null) container.script = text; initGui(); } + @Override + public void close() { + if (parent != null) { + this.save(); + parent.setWorldAndResolution(mc, width, height); + parent.initGui(); + mc.currentScreen = parent; + } else + super.close(); + } + // ==================== FULLSCREEN METHODS ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index 8c22c2dd8..668e2a953 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -1,65 +1,24 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -import noppes.npcs.client.gui.util.GuiNPCInterface; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.LinkedItemScript; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +public class GuiScriptLinkedItem extends GuiScriptInterface { -public class GuiScriptLinkedItem extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNPCManageLinked parent; public final LinkedItem linkedItem; - private final LinkedItemScript scriptHandler; - boolean loaded = false; - - private GuiScriptTextArea textArea; public GuiScriptLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; this.linkedItem = linkedItem; - this.scriptHandler = new LinkedItemScript(); + this.handler = new LinkedItemScript(); + this.singleContainer = true; hookList.add(EnumScriptType.LINKED_ITEM_BUILD.function); hookList.add(EnumScriptType.LINKED_ITEM_VERSION.function); @@ -79,357 +38,26 @@ public GuiScriptLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { LinkedItemScriptPacket.Get(linkedItem.id); } - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - else - textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); - - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; - } - - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.ITEM; } - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - + protected void setHandlerContainer(ScriptContainer container) { + ((LinkedItemScript) handler).container = container; } + @Override public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - + setGuiDataWithOldContainer(compound); } - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - LinkedItemScriptPacket.Save(linkedItem.id, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - LinkedItemScriptPacket.Save(linkedItem.id, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + LinkedItemScriptPacket.Save(linkedItem.id, index, totalCount, scriptNBT); } @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; + public void save() { + saveWithPackets(); } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index 0215fd403..7dea08929 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -1,68 +1,26 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.gui.global.GuiNpcManageRecipes; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -import noppes.npcs.client.gui.util.GuiNPCInterface; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; import noppes.npcs.controllers.data.RecipeScript; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +public class GuiScriptRecipe extends GuiScriptInterface { -public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNpcManageRecipes parent; public final int recipeId; public final boolean anvil; - private final RecipeScript scriptHandler; - boolean loaded = false; - - private GuiScriptTextArea textArea; - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeCarpentry recipe) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; - - this.scriptHandler = new RecipeScript(); + this.handler = new RecipeScript(); this.anvil = false; this.recipeId = recipe.id; + this.singleContainer = true; hookList.add(RecipeScript.ScriptType.PRE.function); hookList.add(RecipeScript.ScriptType.POST.function); @@ -71,14 +29,9 @@ public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeCarpentry recipe) { } public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeAnvil recipe) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; - - this.scriptHandler = new RecipeScript(); + this.handler = new RecipeScript(); this.anvil = true; this.recipeId = recipe.id; @@ -88,355 +41,26 @@ public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeAnvil recipe) { RecipeScriptPacket.Get(true, recipeId); } - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - else - textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); - - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; - } - - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.GLOBAL; } - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - + protected void setHandlerContainer(ScriptContainer container) { + ((RecipeScript) handler).container = container; } + @Override public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - RecipeScriptPacket.Save(anvil, recipeId, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - RecipeScriptPacket.Save(anvil, recipeId, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - + setGuiDataWithOldContainer(compound); } - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, scriptNBT); } - + @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; + public void save() { + saveWithPackets(); } } From ecf1b38763609552706a80bf3339cb286dc962db Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 12 Jan 2026 03:56:22 +0200 Subject: [PATCH 271/337] GuiScript now extends GuiScriptInterface + Full screen support --- .../noppes/npcs/client/gui/GuiScript.java | 337 ++++++++++-------- .../client/gui/script/GuiScriptInterface.java | 125 ++++--- 2 files changed, 259 insertions(+), 203 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index bfe909dee..a911dc88f 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -4,7 +4,6 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiConfirmOpenLink; import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.server.MinecraftServer; @@ -12,6 +11,7 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.script.GuiNPCEventScripts; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.*; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; @@ -19,38 +19,70 @@ import noppes.npcs.controllers.data.DataScript; import noppes.npcs.entity.EntityNPCInterface; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; -public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCallback, ICustomScrollListener, IJTextAreaListener, ITextChangeListener { +/** + * NPC Script GUI - extends GuiScriptInterface but with unique behavior: + * - Uses scroll-based tab selection instead of top buttons + * - Shows settings/script toggle + * - Uses DataScript with getNPCScript(index) access pattern + * - Has console filtering by hook type + */ +public class GuiScript extends GuiScriptInterface { public boolean showScript = false; - private int activeTab = 0; public DataScript script; - public Map> languages = new HashMap>(); - + private static int activeConsole = 0; - boolean loaded = false; - - private Map textAreas = new HashMap<>(); public GuiScript(EntityNPCInterface npc) { - super(npc); - script = npc.script; - drawDefaultBackground = true; - closeOnEsc = true; - xSize = 420; - - setBackground("menubg.png"); + super(); + this.npc = npc; + this.script = npc.script; + this.handler = script; + this.useScrollTabs = true; + this.useSettingsToggle = true; + + // Initialize hook list for NPC scripts + hookList.add("script.init"); + hookList.add("script.update"); + hookList.add("script.interact"); + hookList.add("dialog.dialog"); + hookList.add("script.damaged"); + hookList.add("script.killed"); + hookList.add("script.attack"); + hookList.add("script.target"); + hookList.add("script.collide"); + hookList.add("script.kills"); + hookList.add("script.dialog_closed"); + hookList.add("script.timer"); + hookList.add("script.targetLost"); + hookList.add("script.projectileTick"); + hookList.add("script.projectileImpact"); + NPCScriptPacket.Get(); } + + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.NPC; + } + + @Override + protected int getActiveScriptIndex() { + return this.activeTab; + } + + @Override + protected ScriptContainer getCurrentContainer() { + return script.getNPCScript(activeTab); + } + @Override public void initGui() { super.initGui(); - guiTop += 10; + this.guiTop += 10; + + // ==================== TOP BUTTONS ==================== GuiMenuTopButton top; addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); @@ -60,55 +92,86 @@ public void initGui() { top.active = !showScript; addTopButton(new GuiMenuTopButton(15, top, "gui.website")); - List list = new ArrayList(); - list.add("script.init"); - list.add("script.update"); - list.add("script.interact"); - list.add("dialog.dialog"); - list.add("script.damaged"); - list.add("script.killed"); - list.add("script.attack"); - list.add("script.target"); - list.add("script.collide"); - list.add("script.kills"); - list.add("script.dialog_closed"); - list.add("script.timer"); - list.add("script.targetLost"); - list.add("script.projectileTick"); - list.add("script.projectileImpact"); - if (showScript) { + initScriptView(); + } else { + initSettingsView(); + } + } + + private void initScriptView() { + ScriptContainer container = getCurrentContainer(); + + // ==================== CALCULATE VIEWPORT BOUNDS ==================== + int editorX, editorY, editorWidth, editorHeight; + + if (isFullscreen) { + // Fullscreen: viewport fills screen with configured padding + FullscreenConfig.paddingTop = 30; + FullscreenConfig.paddingBottom = 20; + FullscreenConfig.paddingLeft = 20; + FullscreenConfig.paddingRight = 20; + editorX = FullscreenConfig.paddingLeft; + editorY = FullscreenConfig.paddingTop; + editorWidth = this.width - FullscreenConfig.paddingLeft - FullscreenConfig.paddingRight; + editorHeight = this.height - FullscreenConfig.paddingTop - FullscreenConfig.paddingBottom; + } else { + // Normal: fixed layout with hooks on left + editorX = guiLeft + 74; + editorY = guiTop + 4; + editorWidth = 239; + editorHeight = 208; + } + + // ==================== HOOKS SCROLL (hidden in fullscreen) ==================== + if (!isFullscreen) { addLabel(new GuiNpcLabel(0, "script.hooks", guiLeft + 4, guiTop + 5)); + GuiCustomScroll hooks = new GuiCustomScroll(this, 1); hooks.setSize(68, 198); hooks.guiLeft = guiLeft + 4; hooks.guiTop = guiTop + 14; - hooks.setUnsortedList(list); + hooks.setUnsortedList(hookList); hooks.selected = activeTab; addScroll(hooks); + } - ScriptContainer container = script.getNPCScript(activeTab); + // ==================== SCRIPT TEXT AREA ==================== + int idx = getActiveScriptIndex(); + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea == null) { + activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + activeArea.setListener(this); + this.closeOnEsc(activeArea::closeOnEsc); + textAreas.put(idx, activeArea); + } else { + activeArea.init(editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + } - int idx = this.activeTab; - GuiScriptTextArea activeArea = getActiveScriptArea(); - if (activeArea == null) { - activeArea = new GuiScriptTextArea(this, 2, guiLeft + 74, guiTop + 4, 239, 208, - container == null ? "" : container.script); - activeArea.setListener(this); - this.closeOnEsc(activeArea::closeOnEsc); - textAreas.put(idx, activeArea); - } else { - activeArea.init(guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); + activeArea.setLanguage(script.getLanguage()); + activeArea.setScriptContext(getScriptContext()); + + // Setup fullscreen key binding + GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { + if (e.isPress()) { + toggleFullscreen(); } - - // Set language and script context for proper syntax highlighting and autocomplete - activeArea.setLanguage(script.getLanguage()); - activeArea.setScriptContext(ScriptContext.NPC); - + }); + + activeArea.enableCodeHighlighting(); + addTextField(activeArea); + + // Initialize fullscreen button + int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; + fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); + + // ==================== RIGHT PANEL BUTTONS (hidden in fullscreen) ==================== + if (!isFullscreen) { 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")); - addButton(new GuiNpcButton(107, guiLeft + 315, guiTop + 70, 80, 20, "script.loadscript")); GuiCustomScroll scroll = new GuiCustomScroll(this, 0).setUnselectable(); @@ -118,74 +181,49 @@ public void initGui() { if (container != null) scroll.setList(container.scripts); addScroll(scroll); - } else { - addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); - getTopButton(14).active = true; - addTextField(new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText())); - getTextField(2).canEdit = false; - addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); - addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); - - List l = new ArrayList(); - l.add("All"); - l.addAll(list); - addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, l.toArray(new String[l.size()]), activeConsole)); - - addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); - addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, languages.keySet().toArray(new String[languages.keySet().size()]), getScriptIndex())); - getButton(103).enabled = languages.size() > 0; - - addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); - addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); - - if (MinecraftServer.getServer() != null) - addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); } } - - private GuiScriptTextArea getActiveScriptArea() { - if (this.activeTab > 0) { - int idx = this.activeTab; - if (idx >= 0 && idx < textAreas.size()) - return textAreas.get(idx); - } - return null; + + private void initSettingsView() { + addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); + getTopButton(14).active = true; + + GuiNpcTextArea consoleArea = new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText()); + consoleArea.canEdit = false; + addTextField(consoleArea); + + addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); + addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); + + List consoleOptions = new ArrayList<>(); + consoleOptions.add("All"); + consoleOptions.addAll(hookList); + addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, + consoleOptions.toArray(new String[0]), activeConsole)); + + addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); + addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, + languages.keySet().toArray(new String[0]), getScriptIndex())); + getButton(103).enabled = languages.size() > 0; + + addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); + addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, + new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); + + if (MinecraftServer.getServer() != null) + addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); } @Override - public GuiNpcTextField getTextField(int id) { - if (id == 2) { - GuiScriptTextArea area = getActiveScriptArea(); - if (area != null) - return area; - } - return super.getTextField(id); - } - - private int getScriptIndex() { - int i = 0; - for (String language : languages.keySet()) { - if (language.equalsIgnoreCase(script.scriptLanguage)) - return i; - i++; - } - return 0; - } - - private String getConsoleText() { + protected String getConsoleText() { Map map = this.script.getOldConsoleText(); StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); + for (Map.Entry entry : map.entrySet()) { + builder.insert(0, new Date(entry.getKey()) + entry.getValue() + "\n"); } - return builder.toString(); } - @Override public void confirmClicked(boolean result, int id) { NoppesUtil.openGUI(player, this); @@ -210,8 +248,6 @@ public void confirmClicked(boolean result, int id) { } } } - - displayGuiScreen(this); } @@ -227,8 +263,7 @@ protected void actionPerformed(GuiButton guibutton) { initGui(); } if (guibutton.id == 15) { - GuiConfirmOpenLink guiyesno = new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true); - mc.displayGuiScreen(guiyesno); + displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); } if (guibutton.id == 16) { close(); @@ -241,12 +276,12 @@ protected void actionPerformed(GuiButton guibutton) { NoppesStringUtils.setClipboardContents(getTextField(2).getText()); } if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - displayGuiScreen(guiyesno); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), + StatCollector.translateToLocal("gui.sure"), 101)); } if (guibutton.id == 102) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - displayGuiScreen(guiyesno); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), + StatCollector.translateToLocal("gui.sure"), 102)); } if (guibutton.id == 103) { script.scriptLanguage = ((GuiNpcButton) guibutton).displayString; @@ -259,21 +294,20 @@ protected void actionPerformed(GuiButton guibutton) { initGui(); } if (guibutton.id == 106) { - // TODO: Uses ScriptController.Instance.dir (shared script repository); this GUI is gated behind the scripter tool - // and CustomNpcsPermissions.TOOL_SCRIPTER so only authorized editors touch the controller-backed scripts. NoppesUtil.openFolder(ScriptController.Instance.dir); } if (guibutton.id == 107) { - ScriptContainer container = script.getNPCScript(activeTab); + ScriptContainer container = getCurrentContainer(); if (container == null) script.setNPCScript(activeTab, container = new ScriptContainer(this.script)); setSubGui(new GuiScriptList(languages.get(script.scriptLanguage), container)); } } - private void setScript() { + @Override + protected void setScript() { if (showScript) { - ScriptContainer container = script.getNPCScript(activeTab); + ScriptContainer container = getCurrentContainer(); if (container == null) script.setNPCScript(activeTab, container = new ScriptContainer(this.script)); String text = getTextField(2).getText(); @@ -282,23 +316,31 @@ private void setScript() { container.script = text; } } + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check if click is within autocomplete menu bounds and consume it if so + GuiScriptTextArea activeArea = getActiveScriptArea(); + boolean isOverAutocomplete = activeArea != null + && activeArea.isPointOnAutocompleteMenu(mouseX, mouseY); + if (isOverAutocomplete) { + activeArea.mouseClicked(mouseX, mouseY, mouseButton); + return; + } + + // Check fullscreen button when in script view + if (showScript && !isOverAutocomplete + && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + + super.mouseClicked(mouseX, mouseY, mouseButton); + } @Override public void setGuiData(NBTTagCompound compound) { script.readFromNBT(compound); - NBTTagList data = compound.getTagList("Languages", 10); - Map> languages = new HashMap>(); - for (int i = 0; i < data.tagCount(); i++) { - NBTTagCompound comp = data.getCompoundTagAt(i); - List scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - for (int j = 0; j < list.tagCount(); j++) { - scripts.add(list.getStringTagAt(j)); - } - languages.put(comp.getString("Language"), scripts); - } - this.languages = languages; - initGui(); + loadLanguagesData(compound); loaded = true; } @@ -318,20 +360,5 @@ public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { initGui(); } } - - public void textUpdate(String text) { - ScriptContainer container = script.getNPCScript(activeTab); - if (container != null) - container.script = text; - } - - @Override - public void saveText(String text) { - ScriptContainer container = script.getNPCScript(activeTab); - if (container != null) - container.script = text; - initGui(); - } - } 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 527df14bd..98911941b 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -40,14 +40,20 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected int activeTab = 0; public IScriptHandler handler; public Map> languages = new HashMap(); - private int scriptLimit = 1; + protected int scriptLimit = 1; public List hookList = new ArrayList(); protected boolean loaded = false; - private Map textAreas = new HashMap<>(); + protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - public boolean singleContainer = false; + public boolean singleContainer = false; + + /** If true, uses scroll-based tab selection instead of top buttons */ + protected boolean useScrollTabs = false; + + /** If true, shows settings/script toggle instead of numbered tabs */ + protected boolean useSettingsToggle = false; // ==================== FULLSCREEN MODE ==================== /** Whether the editor viewport is currently in fullscreen mode */ @@ -93,48 +99,54 @@ public void initGui() { int yoffset = (int) ((double) this.ySize * 0.02D); // ==================== TAB BUTTONS ==================== - GuiMenuTopButton top; - int topButtonsX = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingLeft : this.guiLeft + 4; - int topButtonsY = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingTop - 20 : this.guiTop - 17; - this.addTopButton(top = new GuiMenuTopButton(0, topButtonsX, topButtonsY, "gui.settings")); - - int topXoffset = 0; - int topYoffset = 0; - if(!singleContainer) { - for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { - if (ta % 20 == 0 && ta > 0) { - topYoffset -= 20; - topXoffset -= top.width + 20 * 22; + // Skip standard tab creation for GUIs that use custom toggle/scroll tabs + if (!useSettingsToggle) { + GuiMenuTopButton top; + int topButtonsX = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingLeft : this.guiLeft + 4; + int topButtonsY = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingTop - 20 : this.guiTop - 17; + this.addTopButton(top = new GuiMenuTopButton(0, topButtonsX, topButtonsY, "gui.settings")); + + int topXoffset = 0; + int topYoffset = 0; + if(!singleContainer) { + for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { + if (ta % 20 == 0 && ta > 0) { + topYoffset -= 20; + topXoffset -= top.width + 20 * 22; + } + this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, + top.yPosition + topYoffset, ta + 1 + "")); + topXoffset = 0; + topYoffset = 0; + scriptLimit = ta + 2; } - this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, - top.yPosition + topYoffset, ta + 1 + "")); - topXoffset = 0; - topYoffset = 0; - scriptLimit = ta + 2; } - } - if (singleContainer && getFirst() != null) - this.addTopButton( - top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, - "Script")); - else if (this.handler.getScripts().size() < 100) - this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); + if (singleContainer && getFirst() != null) + this.addTopButton( + top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, + "Script")); + else if (this.handler.getScripts().size() < 100) + this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); + top = this.getTopButton(this.activeTab); + if (top == null) { + this.activeTab = 0; + top = this.getTopButton(0); + } + top.active = true; } - top.active = true; // ==================== SCRIPT EDITOR TAB (activeTab > 0) ==================== - if (this.activeTab > 0) { - initScriptEditorTab(yoffset); - } else { - // ==================== SETTINGS TAB (activeTab == 0) ==================== - initSettingsTab(yoffset); + // Skip for GUIs that handle their own view initialization (useSettingsToggle) + if (!useSettingsToggle) { + if (this.activeTab > 0) { + initScriptEditorTab(yoffset); + } else { + // ==================== SETTINGS TAB (activeTab == 0) ==================== + initSettingsTab(yoffset); + } } this.xSize = 420; @@ -146,7 +158,7 @@ else if (this.handler.getScripts().size() < 100) * Handles both normal and fullscreen modes. */ private void initScriptEditorTab(int yoffset) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + ScriptContainer container = getCurrentContainer(); // ==================== CALCULATE VIEWPORT BOUNDS ==================== int editorX, editorY, editorWidth, editorHeight; @@ -190,7 +202,7 @@ private void initScriptEditorTab(int yoffset) { } // ==================== SCRIPT TEXT AREA ==================== - int idx = this.activeTab - 1; + int idx = getActiveScriptIndex(); GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, @@ -401,11 +413,28 @@ public void confirmClicked(boolean flag, int i) { } public GuiScriptTextArea getActiveScriptArea() { - if (this.activeTab > 0) { - int idx = this.activeTab - 1; - if (textAreas.containsKey(idx)) - return textAreas.get(idx); - } + int idx = getActiveScriptIndex(); + if (idx >= 0 && textAreas.containsKey(idx)) + return textAreas.get(idx); + return null; + } + + /** + * Get the current script index for text area storage. + * Override in subclasses with different tab logic. + */ + protected int getActiveScriptIndex() { + return this.activeTab - 1; + } + + /** + * Get the current script container. + * Override in subclasses with different container access patterns. + */ + protected ScriptContainer getCurrentContainer() { + int idx = getActiveScriptIndex(); + if (idx >= 0 && idx < this.handler.getScripts().size()) + return this.handler.getScripts().get(idx); return null; } @@ -511,8 +540,8 @@ protected void actionPerformed(GuiButton guibutton) { } protected void setScript() { - if (this.activeTab > 0) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + if (this.activeTab > 0 || useScrollTabs) { + ScriptContainer container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) @@ -655,7 +684,7 @@ public void save() { } public void textUpdate(String text) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + ScriptContainer container = getCurrentContainer(); if (container != null) { container.script = text; } @@ -666,7 +695,7 @@ public void textUpdate(String text) { @Override //NEVER USED public void saveText(String text) { - ScriptContainer container = handler.getScripts().get(activeTab ); //activeTab - 1? + ScriptContainer container = getCurrentContainer(); if (container != null) container.script = text; initGui(); From 818acfb00bd8b2891f9b7f9128d6ca4d31018252 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 17:34:14 +0200 Subject: [PATCH 272/337] Abstracted ScriptContainer & JaninoScript to IScriptUnit + global implementation --- .../java/kamkeel/npcs/network/PacketUtil.java | 5 +- .../script/item/ItemScriptErrorPacket.java | 10 +- src/main/java/noppes/npcs/NBTTags.java | 16 +- .../npcs/blocks/tiles/TileScripted.java | 21 ++- .../client/gui/script/GuiNPCEventScripts.java | 5 +- .../client/gui/script/GuiScriptAllNPCs.java | 5 +- .../client/gui/script/GuiScriptForge.java | 5 +- .../client/gui/script/GuiScriptInterface.java | 31 ++-- .../client/gui/script/GuiScriptPlayers.java | 5 +- .../npcs/controllers/ScriptContainer.java | 73 +++++++- .../npcs/controllers/data/DataScript.java | 24 +-- .../npcs/controllers/data/EffectScript.java | 34 ++-- .../controllers/data/ForgeDataScript.java | 40 +++-- .../controllers/data/GlobalNPCDataScript.java | 40 +++-- .../npcs/controllers/data/IScriptHandler.java | 5 +- .../npcs/controllers/data/IScriptUnit.java | 117 +++++++++++++ .../controllers/data/LinkedItemScript.java | 32 ++-- .../controllers/data/PlayerDataScript.java | 69 ++++---- .../npcs/controllers/data/RecipeScript.java | 19 +- .../java/noppes/npcs/janino/JaninoScript.java | 163 ++++++++++++++---- .../npcs/scripted/item/ScriptCustomItem.java | 17 +- 21 files changed, 517 insertions(+), 219 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/IScriptUnit.java diff --git a/src/main/java/kamkeel/npcs/network/PacketUtil.java b/src/main/java/kamkeel/npcs/network/PacketUtil.java index f262bce45..cb2881eb8 100644 --- a/src/main/java/kamkeel/npcs/network/PacketUtil.java +++ b/src/main/java/kamkeel/npcs/network/PacketUtil.java @@ -15,6 +15,7 @@ import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.items.ItemNpcTool; import java.io.IOException; @@ -221,9 +222,9 @@ public static void getScripts(IScriptHandler data, EntityPlayerMP player) { compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(data.getConsoleText())); GuiDataPacket.sendGuiData(player, compound); - List containers = data.getScripts(); + List containers = data.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); NBTTagCompound tabCompound = new NBTTagCompound(); tabCompound.setInteger("Tab", i); tabCompound.setTag("Script", container.writeToNBT(new NBTTagCompound())); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java index b4a8c619e..c1a7c74c5 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java @@ -16,7 +16,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemStack; import noppes.npcs.config.ConfigScript; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.item.ScriptCustomItem; @@ -72,10 +72,10 @@ public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { if (requestedAction == Action.GET) { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : ((ScriptCustomItem) iw).scripts) { + for (IScriptUnit script : ((ScriptCustomItem) iw).scripts) { ++tab; - for (Map.Entry longStringEntry : script.console.entrySet()) { + for (Map.Entry longStringEntry : script.getConsole().entrySet()) { map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); } } @@ -84,8 +84,8 @@ public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { compound.setTag("ItemScriptConsole", NBTTags.NBTLongStringMap(map)); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } else { - for (ScriptContainer script : ((ScriptCustomItem) iw).scripts) { - script.console.clear(); + for (IScriptUnit script : ((ScriptCustomItem) iw).scripts) { + script.clearConsole(); } } } diff --git a/src/main/java/noppes/npcs/NBTTags.java b/src/main/java/noppes/npcs/NBTTags.java index ebf30ab94..21e751f11 100644 --- a/src/main/java/noppes/npcs/NBTTags.java +++ b/src/main/java/noppes/npcs/NBTTags.java @@ -8,6 +8,7 @@ import net.minecraft.nbt.NBTTagList; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.HashMap; @@ -540,8 +541,8 @@ public static NBTTagList NBTLongStringMap(Map map) { } } - public static List GetScriptOld(NBTTagList list, IScriptHandler handler) { - ArrayList scripts = new ArrayList(); + public static List GetScriptOld(NBTTagList list, IScriptHandler handler) { + ArrayList scripts = new ArrayList<>(); for (int i = 0; i < list.tagCount(); ++i) { NBTTagCompound compoundd = list.getCompoundTagAt(i); @@ -553,8 +554,8 @@ public static List GetScriptOld(NBTTagList list, IScriptHandler return scripts; } - public static List GetScript(NBTTagCompound compound, IScriptHandler handler) { - ArrayList scripts = new ArrayList<>(); + public static List GetScript(NBTTagCompound compound, IScriptHandler handler) { + ArrayList scripts = new ArrayList<>(); for (int i = 0; i < compound.getInteger("TotalScripts"); ++i) { NBTTagCompound containerCompound = compound.getCompoundTag("Tab" + i); @@ -566,12 +567,9 @@ public static List GetScript(NBTTagCompound compound, IScriptHa return scripts; } - public static NBTTagList NBTScript(List scripts) { + public static NBTTagList NBTScript(List scripts) { NBTTagList list = new NBTTagList(); - Iterator var2 = scripts.iterator(); - - while (var2.hasNext()) { - ScriptContainer script = (ScriptContainer) var2.next(); + for (IScriptUnit script : scripts) { NBTTagCompound compound = new NBTTagCompound(); script.writeToNBT(compound); list.appendTag(compound); diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 8babf8936..2ecdb518e 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -28,6 +28,7 @@ import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptBlockHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.data.DataTimers; import noppes.npcs.scripted.BlockScriptedWrapper; import noppes.npcs.util.ValueUtil; @@ -40,7 +41,7 @@ import java.util.TreeMap; public class TileScripted extends TileEntity implements IScriptBlockHandler { - public List scripts = new ArrayList<>(); + public List scripts = new ArrayList<>(); public Map tempData = new HashMap<>(); @@ -367,8 +368,10 @@ public void callScript(EnumScriptType type, Event event) { EventHooks.onScriptBlockInit(this); } - for (ScriptContainer script : scripts) { - script.run(type, event); + for (IScriptUnit script : scripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).run(type, event); + } } } @@ -408,12 +411,12 @@ public void setLanguage(String lang) { } @Override - public void setScripts(List list) { + public void setScripts(List list) { scripts = list; } @Override - public List getScripts() { + public List getScripts() { return scripts; } @@ -421,9 +424,9 @@ public List getScripts() { public Map getConsoleText() { Map map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : getScripts()) { + for (IScriptUnit script : getScripts()) { tab++; - for (Entry entry : script.console.entrySet()) { + for (Entry entry : script.getConsole().entrySet()) { map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } @@ -432,8 +435,8 @@ public Map getConsoleText() { @Override public void clearConsole() { - for (ScriptContainer script : getScripts()) { - script.console.clear(); + for (IScriptUnit script : getScripts()) { + script.clearConsole(); } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index 5b4693a10..a2811b1a0 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -5,6 +5,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.DataScript; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; @@ -65,9 +66,9 @@ public void setGuiData(NBTTagCompound compound) { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); EventScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java index 6f0927cbc..492945d71 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java @@ -5,6 +5,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.GlobalNPCDataScript; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.List; @@ -61,9 +62,9 @@ public void setGuiData(NBTTagCompound compound) { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); GlobalNPCScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java index aea39d1a5..31c73478c 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java @@ -15,6 +15,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.ForgeDataScript; +import noppes.npcs.controllers.data.IScriptUnit; import org.apache.commons.lang3.StringUtils; import java.io.IOException; @@ -91,9 +92,9 @@ public void setGuiData(NBTTagCompound compound) { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); ForgeScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); 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 98911941b..04d4f0595 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.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -158,7 +159,7 @@ else if (this.handler.getScripts().size() < 100) * Handles both normal and fullscreen modes. */ private void initScriptEditorTab(int yoffset) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); // ==================== CALCULATE VIEWPORT BOUNDS ==================== int editorX, editorY, editorWidth, editorHeight; @@ -206,13 +207,13 @@ private void initScriptEditorTab(int yoffset) { GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + container == null ? "" : container.getScript()); activeArea.setListener(this); this.closeOnEsc(activeArea::closeOnEsc); textAreas.put(idx, activeArea); } else { activeArea.init(editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + container == null ? "" : container.getScript()); } // Set the scripting language for proper syntax highlighting @@ -249,7 +250,7 @@ private void initScriptEditorTab(int yoffset) { scroll.guiLeft = left1; scroll.guiTop = this.guiTop + 88 + yoffset; if (container != null) { - scroll.setList(container.scripts); + scroll.setList(container.getExternalScripts()); } this.addScroll(scroll); } @@ -431,7 +432,7 @@ protected int getActiveScriptIndex() { * Get the current script container. * Override in subclasses with different container access patterns. */ - protected ScriptContainer getCurrentContainer() { + protected IScriptUnit getCurrentContainer() { int idx = getActiveScriptIndex(); if (idx >= 0 && idx < this.handler.getScripts().size()) return this.handler.getScripts().get(idx); @@ -448,7 +449,7 @@ public GuiNpcTextField getTextField(int id) { return super.getTextField(id); } - public ScriptContainer getFirst() { + public IScriptUnit getFirst() { if (this.handler.getScripts().isEmpty()) return null; return this.handler.getScripts().get(0); @@ -541,7 +542,7 @@ protected void actionPerformed(GuiButton guibutton) { protected void setScript() { if (this.activeTab > 0 || useScrollTabs) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) @@ -553,7 +554,7 @@ protected void setScript() { String text = (this.getTextField(2)).getText(); text = text.replace("\r\n", "\n"); text = text.replace("\r", "\n"); - container.script = text; + container.setScript(text); } } @@ -640,7 +641,7 @@ protected void loadLanguagesData(NBTTagCompound compound) { /** * Set the handler's container. Override if handler is not IScriptHandler. */ - protected void setHandlerContainer(ScriptContainer container) { + protected void setHandlerContainer(IScriptUnit container) { // Default implementation - subclasses may need to cast and set differently // e.g., ((LinkedItemScript) handler).container = container; } @@ -653,9 +654,9 @@ protected void saveWithPackets() { if (loaded) { this.setScript(); - List containers = this.handler.getScripts(); + List containers = this.handler.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } @@ -684,9 +685,9 @@ public void save() { } public void textUpdate(String text) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); if (container != null) { - container.script = text; + container.setScript(text); } } @@ -695,9 +696,9 @@ public void textUpdate(String text) { @Override //NEVER USED public void saveText(String text) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); if (container != null) - container.script = text; + container.setScript(text); initGui(); } 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 9bb3ebdfd..4d2bffde2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java @@ -8,6 +8,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.EffectScript; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.data.PlayerDataScript; import java.util.List; @@ -123,9 +124,9 @@ protected ScriptContext getScriptContext() { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); PlayerScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index 5b2c21cdd..f8649ac47 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -7,6 +7,7 @@ import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.NpcAPI; import javax.script.Compilable; @@ -24,7 +25,7 @@ import java.util.Objects; import java.util.TreeMap; -public class ScriptContainer { +public class ScriptContainer implements IScriptUnit { private static final String lock = "lock"; public static ScriptContainer Current; private static String CurrentType; @@ -251,6 +252,7 @@ public boolean isValid() { return evaluated && !errored; } + @Override public boolean hasCode() { if (!scripts.isEmpty()) return true; @@ -267,4 +269,73 @@ public void setEngine(String scriptLanguage) { this.evaluated = false; } } + + // ==================== IScriptUnit IMPLEMENTATION ==================== + + @Override + public String getScript() { + return this.script; + } + + @Override + public void setScript(String script) { + this.script = script; + this.evaluated = false; + } + + @Override + public List getExternalScripts() { + return this.scripts; + } + + @Override + public void setExternalScripts(List scripts) { + this.scripts = scripts; + this.evaluated = false; + } + + @Override + public TreeMap getConsole() { + return this.console; + } + + @Override + public void clearConsole() { + this.console.clear(); + } + + @Override + public String getLanguage() { + // If a specific language is set for this container, use it + // Otherwise fall back to the handler's language + if (currentScriptLanguage != null) { + return currentScriptLanguage; + } + return handler != null ? handler.getLanguage() : "ECMAScript"; + } + + @Override + public void setLanguage(String language) { + if (!Objects.equals(this.currentScriptLanguage, language)) { + this.currentScriptLanguage = language; + this.evaluated = false; + } + } + + @Override + public String generateHookStub(String hookName, Object hookData) { + // ECMAScript function template + return "function " + hookName + "(event) {\n \n}\n"; + } + + @Override + public boolean hasErrored() { + return this.errored; + } + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } } + diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index bf4327d38..45ba9a966 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -36,7 +36,7 @@ import java.util.TreeMap; public class DataScript implements INpcScriptHandler { - public List eventScripts = new ArrayList<>(); + public List eventScripts = new ArrayList<>(); private HashMap scripts = new HashMap<>(); private final static EntityType entities = new EntityType(); @@ -146,14 +146,16 @@ public boolean callScript(EnumScriptType type, Event event, Object... obs) { if (!hasInited && !npc.isRemote() && type != EnumScriptType.INIT) { hasInited = true; - for (ScriptContainer scriptContainer : this.eventScripts) { - scriptContainer.errored = false; + for (IScriptUnit scriptUnit : this.eventScripts) { + scriptUnit.setErrored(false); } EventHooks.onNPCInit(this.npc); } - for (ScriptContainer script : this.eventScripts) { - script.run(type, event); + for (IScriptUnit script : this.eventScripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).run(type, event); + } } ScriptContainer script = scripts.get(type); @@ -224,10 +226,10 @@ public Map getOldConsoleText() { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - for (Map.Entry longStringEntry : script.console.entrySet()) { + for (Map.Entry longStringEntry : script.getConsole().entrySet()) { map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); } } @@ -235,8 +237,8 @@ public Map getConsoleText() { } public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } @@ -274,11 +276,11 @@ public void setLanguage(String lang) { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.eventScripts = list; } - public List getScripts() { + public List getScripts() { return this.eventScripts; } diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index bd2b75dae..497b46421 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -15,13 +15,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; public class EffectScript implements INpcScriptHandler { - public ScriptContainer container; + public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -64,7 +63,8 @@ public void callScript(String s, Event event) { if (!this.isEnabled()) { return; } - container.run(s, event); + if (container instanceof ScriptContainer) + ((ScriptContainer) container).run(s, event); } @Override @@ -93,7 +93,7 @@ public void setLanguage(String s) { } @Override - public void setScripts(List list) { + public void setScripts(List list) { if (list == null || list.isEmpty()) { container = null; return; @@ -102,7 +102,7 @@ public void setScripts(List list) { } @Override - public List getScripts() { + public List getScripts() { if (container == null) return new ArrayList<>(); return Collections.singletonList(container); @@ -115,31 +115,21 @@ public String noticeString() { @Override public Map getConsoleText() { - TreeMap map = new TreeMap(); + TreeMap map = new TreeMap<>(); int tab = 0; - Iterator var3 = this.getScripts().iterator(); - - while (var3.hasNext()) { - ScriptContainer script = (ScriptContainer) var3.next(); + for (IScriptUnit script : this.getScripts()) { ++tab; - Iterator var5 = script.console.entrySet().iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } - return map; } @Override public void clearConsole() { - Iterator var1 = this.getScripts().iterator(); - - while (var1.hasNext()) { - ScriptContainer script = (ScriptContainer) var1.next(); - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } @@ -180,4 +170,4 @@ public enum ScriptType { this.function = functionName; } } -} +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index dde123aae..d38342f0e 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -16,7 +16,7 @@ import java.util.TreeMap; public class ForgeDataScript implements INpcScriptHandler { - private List scripts = new ArrayList(); + private List scripts = new ArrayList<>(); private String scriptLanguage = "ECMAScript"; public long lastInited = -1L; private long lastForgeUpdate = -1L; @@ -26,14 +26,14 @@ public ForgeDataScript() { } public void clear() { - this.scripts = new ArrayList(); + this.scripts = new ArrayList<>(); } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); } else { - this.scripts = NBTTags.GetScript(compound, this); + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); } this.scriptLanguage = compound.getString("ScriptLanguage"); if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { @@ -67,8 +67,9 @@ public void callScript(String type, Event event) { this.lastInited = ScriptController.Instance.lastLoaded; this.lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; - for (ScriptContainer script : this.scripts) { - script.errored = false; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; } if (!type.equals("init")) { @@ -76,11 +77,15 @@ public void callScript(String type, Event event) { } } - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) + for (IScriptUnit script : this.scripts) { + if (script == null || !script.hasCode()) continue; - - script.run(type, event); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + if (container.errored) + continue; + container.run(type, event); + } } } } @@ -109,11 +114,11 @@ public void setLanguage(String lang) { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } @@ -124,19 +129,18 @@ public String noticeString() { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - - for (Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); + for (Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } return map; } public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index 335256e0f..f21818136 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -27,7 +27,7 @@ import java.util.TreeMap; public class GlobalNPCDataScript implements INpcScriptHandler { - public List scripts = new ArrayList(); + public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityNPCInterface npc; private ICustomNpc npcAPI; @@ -42,14 +42,14 @@ public GlobalNPCDataScript(EntityNPCInterface npc) { } public void clear() { - this.scripts = new ArrayList(); + this.scripts = new ArrayList<>(); } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); } else { - this.scripts = NBTTags.GetScript(compound, this); + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); } this.scriptLanguage = compound.getString("ScriptLanguage"); if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { @@ -87,16 +87,21 @@ public void callScript(String hookName, Event event) { this.lastInited = ScriptController.Instance.lastLoaded; this.lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; - for (ScriptContainer script : this.scripts) { - script.errored = false; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; } } - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) + for (IScriptUnit script : this.scripts) { + if (script == null || !script.hasCode()) continue; - - script.run(hookName, event); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + if (container.errored) + continue; + container.run(hookName, event); + } } } } @@ -121,11 +126,11 @@ public void setLanguage(String lang) { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } @@ -148,19 +153,18 @@ public ICustomNpc getNpc() { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - - for (Map.Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } return map; } public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index be37c86f1..d89fbc7a2 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -1,7 +1,6 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.controllers.ScriptContainer; import java.util.List; import java.util.Map; @@ -19,9 +18,9 @@ public interface IScriptHandler { void setLanguage(String language); - void setScripts(List list); + void setScripts(List list); - List getScripts(); + List getScripts(); String noticeString(); diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java new file mode 100644 index 000000000..d8d280be0 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -0,0 +1,117 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Common interface for script units that can be edited in a script GUI. + * Implemented by both ScriptContainer (ECMAScript) and JaninoScript (Java/Janino). + * This allows GuiScriptInterface to work with both script types uniformly. + */ +public interface IScriptUnit { + + // ==================== SCRIPT CONTENT ==================== + + /** + * Get the main script text content. + */ + String getScript(); + + /** + * Set the main script text content. + */ + void setScript(String script); + + /** + * Get the list of external script file names loaded for this unit. + */ + List getExternalScripts(); + + /** + * Set the list of external script file names to load. + */ + void setExternalScripts(List scripts); + + // ==================== CONSOLE ==================== + + /** + * Get the console output map (timestamp -> message). + */ + TreeMap getConsole(); + + /** + * Clear all console output. + */ + void clearConsole(); + + /** + * Append a message to the console. + */ + void appendConsole(String message); + + // ==================== LANGUAGE ==================== + + /** + * Get the scripting language for this unit. + * Returns "ECMAScript", "Java", etc. + */ + String getLanguage(); + + /** + * Set the scripting language for this unit. + * @param language The language name (e.g., "ECMAScript", "Java") + */ + void setLanguage(String language); + + /** + * Check if this script unit uses Janino (Java) compilation. + */ + default boolean isJanino() { + return "Java".equals(getLanguage()); + } + + // ==================== HOOK GENERATION ==================== + + /** + * Generate a stub/template for the given hook name. + * ECMAScript: function hookName(event) { } + * Janino: full Java method signature with return type + * + * @param hookName The hook name to generate a stub for + * @param hookData Optional data (e.g., Method object for Janino) + * @return The generated stub string + */ + String generateHookStub(String hookName, Object hookData); + + // ==================== NBT ==================== + + /** + * Write this script unit to NBT. + */ + NBTTagCompound writeToNBT(NBTTagCompound compound); + + /** + * Read this script unit from NBT. + */ + void readFromNBT(NBTTagCompound compound); + + // ==================== STATE ==================== + + /** + * Check if this script unit has any code (script or external scripts). + */ + boolean hasCode(); + + /** + * Check if this script unit has encountered an error during execution. + */ + boolean hasErrored(); + + /** + * Set the error state of this script unit. + */ + void setErrored(boolean errored); +} diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index cce8df255..18df07229 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -14,13 +14,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; public class LinkedItemScript implements INpcScriptHandler { - public ScriptContainer container; + public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -58,7 +57,8 @@ public void callScript(String s, Event event) { if (!this.isEnabled()) { return; } - container.run(s, event); + if (container instanceof ScriptContainer) + ((ScriptContainer) container).run(s, event); } @Override @@ -87,7 +87,7 @@ public void setLanguage(String s) { } @Override - public void setScripts(List list) { + public void setScripts(List list) { if (list == null || list.isEmpty()) { container = null; return; @@ -96,7 +96,7 @@ public void setScripts(List list) { } @Override - public List getScripts() { + public List getScripts() { if (container == null) return new ArrayList<>(); return Collections.singletonList(container); @@ -109,31 +109,21 @@ public String noticeString() { @Override public Map getConsoleText() { - TreeMap map = new TreeMap(); + TreeMap map = new TreeMap<>(); int tab = 0; - Iterator var3 = this.getScripts().iterator(); - - while (var3.hasNext()) { - ScriptContainer script = (ScriptContainer) var3.next(); + for (IScriptUnit script : this.getScripts()) { ++tab; - Iterator var5 = script.console.entrySet().iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } - return map; } @Override public void clearConsole() { - Iterator var1 = this.getScripts().iterator(); - - while (var1.hasNext()) { - ScriptContainer script = (ScriptContainer) var1.next(); - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index a425bdcd4..5d2eee9f9 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -31,7 +31,7 @@ import java.util.TreeMap; public class PlayerDataScript implements INpcScriptHandler { - public List scripts = new ArrayList(); + public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityPlayer player; private IPlayer playerAPI; @@ -53,14 +53,14 @@ public PlayerDataScript(EntityPlayer player) { public void clear() { console = new TreeMap(); errored = new ArrayList(); - scripts = new ArrayList(); + scripts = new ArrayList<>(); } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); } else { - this.scripts = NBTTags.GetScript(compound, this); + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); } this.scriptLanguage = compound.getString("ScriptLanguage"); if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { @@ -104,10 +104,12 @@ public void callScript(String hookName, Event event) { errored.clear(); if (player != null) { scripts.clear(); - for (ScriptContainer script : ScriptController.Instance.playerScripts.scripts) { - ScriptContainer s = new ScriptContainer(this); - s.readFromNBT(script.writeToNBT(new NBTTagCompound())); - scripts.add(s); + for (IScriptUnit script : ScriptController.Instance.playerScripts.scripts) { + if (script instanceof ScriptContainer) { + ScriptContainer s = new ScriptContainer(this); + s.readFromNBT(((ScriptContainer) script).writeToNBT(new NBTTagCompound())); + scripts.add(s); + } } } lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; @@ -117,26 +119,30 @@ public void callScript(String hookName, Event event) { } } for (int i = 0; i < scripts.size(); i++) { - ScriptContainer script = scripts.get(i); + IScriptUnit script = scripts.get(i); if (errored.contains(i)) continue; - script.run(hookName, event); - if (script.errored) { - errored.add(i); - } - for (Entry entry : script.console.entrySet()) { - if (!console.containsKey(entry.getKey())) - console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + container.run(hookName, event); + if (container.errored) { + errored.add(i); + } + for (Entry entry : container.console.entrySet()) { + if (!console.containsKey(entry.getKey())) + console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + } + container.console.clear(); } - script.console.clear(); } } else { if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastPlayerUpdate > this.lastPlayerUpdate) { this.lastInited = ScriptController.Instance.lastLoaded; this.lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; - for (ScriptContainer scriptContainer : this.scripts) { - scriptContainer.errored = false; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; } if (!Objects.equals(hookName, EnumScriptType.INIT.function) && event instanceof PlayerEvent) { @@ -145,11 +151,13 @@ public void callScript(String hookName, Event event) { } } - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored()|| !script.hasCode()) continue; - - script.run(hookName, event); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + container.run(hookName, event); + } } } } @@ -175,11 +183,11 @@ public void setLanguage(String lang) { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } @@ -205,11 +213,10 @@ public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - - for (Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); + for (Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } return map; @@ -219,8 +226,8 @@ public void clearConsole() { if (ConfigScript.IndividualPlayerScripts) console.clear(); else { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index b94acfbc6..b42595cf5 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -19,7 +19,7 @@ import java.util.TreeMap; public class RecipeScript implements INpcScriptHandler { - public ScriptContainer container; + public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -54,7 +54,8 @@ public void callScript(EnumScriptType type, Event event) { public void callScript(String s, Event event) { if (!this.isEnabled()) return; - container.run(s, event); + if (container instanceof ScriptContainer) + ((ScriptContainer) container).run(s, event); } @Override @@ -83,7 +84,7 @@ public void setLanguage(String s) { } @Override - public void setScripts(List list) { + public void setScripts(List list) { if (list == null || list.isEmpty()) { container = null; return; @@ -92,7 +93,7 @@ public void setScripts(List list) { } @Override - public List getScripts() { + public List getScripts() { if (container == null) return new ArrayList<>(); return Collections.singletonList(container); @@ -107,9 +108,9 @@ public String noticeString() { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - for (Map.Entry entry : script.console.entrySet()) { + for (Map.Entry entry : script.getConsole().entrySet()) { map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } @@ -118,8 +119,8 @@ public Map getConsoleText() { @Override public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } @@ -156,4 +157,4 @@ public enum ScriptType { this.function = functionName; } } -} +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index f836e97cf..5fb02a46e 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -8,16 +8,21 @@ import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.IScriptUnit; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.security.*; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; -public abstract class JaninoScript { +public abstract class JaninoScript implements IScriptUnit { public boolean enabled; + public boolean errored = false; private String language = "Java"; public TreeMap console = new TreeMap(); @@ -184,18 +189,132 @@ private void appendExternalScripts(StringBuilder sb) { sb.append(code).append("\n"); } } + + // ==================== IScriptUnit IMPLEMENTATION ==================== + @Override + public String getScript() { + return this.script; + } + + @Override public void setScript(String script) { this.script = script; this.evaluated = false; } + + @Override + public List getExternalScripts() { + return this.externalScripts; + } + @Override public void setExternalScripts(List externalScripts) { this.externalScripts = externalScripts; this.evaluated = false; } + + @Override + public TreeMap getConsole() { + return this.console; + } + + @Override + public void clearConsole() { + console.clear(); + } + + @Override + public void appendConsole(String message) { + if (message != null && !message.isEmpty()) { + long time = System.currentTimeMillis(); + if (this.console.containsKey(time)) { + message = (String) this.console.get(time) + "\n" + message; + } + + this.console.put(time, message); + while (this.console.size() > 40) { + this.console.remove(this.console.firstKey()); + } + } + } + + @Override + public String getLanguage() { + return this.language; + } + + @Override + public void setLanguage(String language) { + // JaninoScript is always Java, ignore attempts to change + // This method exists for IScriptUnit interface compatibility + } + + @Override + public boolean hasCode() { + if (!externalScripts.isEmpty()) + return true; + return script != null && !script.isEmpty(); + } + + @Override + public String generateHookStub(String hookName, Object hookData) { + // If hookData is a Method, generate full Java method stub + if (hookData instanceof Method) { + return generateMethodStub((Method) hookData); + } + // Fallback for simple hook names - generate basic override + return String.format("@Override\npublic void %s() {\n \n}\n", hookName); + } + + /** + * Generates a stub string for a given Method. + */ + public static String generateMethodStub(Method method) { + String mods = Modifier.toString(method.getModifiers()); + // Remove 'abstract' from modifiers for implementation + mods = mods.replace("abstract ", "").replace("abstract", "").trim(); + if (!mods.isEmpty()) + mods += " "; + + String returnTypeStr = method.getReturnType().getSimpleName(); + String name = method.getName(); + + Map typeCount = new HashMap<>(); + String params = Arrays.stream(method.getParameters()) + .map(p -> { + String typeName = p.getType().getSimpleName(); + String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + + String paramName = count == 1 ? baseName : baseName + (count - 1); + return typeName + " " + paramName; + }) + .collect(Collectors.joining(", ")); + + String body; + Class returnType = method.getReturnType(); + if (returnType == void.class) + body = ""; + else if (returnType.isPrimitive()) { + if (returnType == boolean.class) + body = " return false;"; + else if (returnType == char.class) + body = " return '\\0';"; + else + body = " return 0;"; + } else { + body = " return null;"; + } + return String.format("@Override\n%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + + + @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setBoolean("enabled", enabled); compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); @@ -204,12 +323,12 @@ public NBTTagCompound writeToNBT(NBTTagCompound compound) { return compound; } - public JaninoScript readFromNBT(NBTTagCompound compound) { + @Override + public void readFromNBT(NBTTagCompound compound) { this.enabled = compound.getBoolean("enabled"); this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); - setScript(compound.getString("script")); //after everything else is read - return this; + setScript(compound.getString("script")); } public boolean isEnabled() { @@ -233,25 +352,6 @@ public void setEnabled(boolean b) { } } - public String getLanguage() { - return this.language; - } - - public void appendConsole(String message) { - if (message != null && !message.isEmpty()) { - long time = System.currentTimeMillis(); - if (this.console.containsKey(time)) { - message = (String) this.console.get(time) + "\n" + message; - } - - this.console.put(time, message); - - while (this.console.size() > 40) { - this.console.remove(this.console.firstKey()); - } - } - } - public Map getConsoleText() { TreeMap map = new TreeMap(); int tab = 0; @@ -264,13 +364,16 @@ public Map getConsoleText() { map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); } - return map; } - - public void clearConsole() { - console.clear(); + + @Override + public boolean hasErrored() { + return this.errored; } -} - - + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 6e85e1555..e03995e16 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -7,6 +7,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.INpcScriptHandler; @@ -19,7 +20,7 @@ import java.util.TreeMap; public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, INpcScriptHandler { - public List scripts = new ArrayList(); + public List scripts = new ArrayList(); public List errored = new ArrayList(); public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -95,14 +96,16 @@ public void callScript(String hookName, Event event) { } for (int i = 0; i < this.scripts.size(); i++) { - ScriptContainer script = this.scripts.get(i); + IScriptUnit script = this.scripts.get(i); if (!this.errored.contains(i)) { - if (script == null || script.errored || !script.hasCode()) + if (script == null || script.hasErrored() || !script.hasCode()) continue; - script.run(hookName, event); + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).run(hookName, event); + } - if (script.errored) { + if (script.hasErrored()) { this.errored.add(i); } } @@ -129,11 +132,11 @@ public void setLanguage(String lang) { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } From 95e7b3255ed8323cff2617641940d60951a7724e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 18:14:38 +0200 Subject: [PATCH 273/337] resolved merge conflicts with dev --- .../client/gui/script/GuiScriptBlock.java | 3 ++ .../client/gui/script/GuiScriptEffect.java | 4 ++ .../client/gui/script/GuiScriptInterface.java | 45 ++++++++++++++++++- .../npcs/client/gui/script/GuiScriptItem.java | 7 ++- .../gui/script/GuiScriptLinkedItem.java | 4 ++ .../client/gui/script/GuiScriptRecipe.java | 4 ++ .../npcs/controllers/data/IScriptHandler.java | 19 ++++++++ .../java/noppes/npcs/scripted/NpcAPI.java | 17 +------ 8 files changed, 82 insertions(+), 21 deletions(-) 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 6f7b1da99..b11947ee7 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java @@ -2,9 +2,12 @@ import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.blocks.tiles.TileScripted; import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; +import java.util.ArrayList; public class GuiScriptBlock extends GuiScriptInterface { private final TileScripted tileScripted; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index 0adf8e29a..b7d6e8674 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -2,12 +2,16 @@ import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.client.gui.global.GuiNPCManageEffects; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.CustomEffect; import noppes.npcs.controllers.data.EffectScript; +import java.util.ArrayList; + public class GuiScriptEffect extends GuiScriptInterface { public final CustomEffect effect; 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 04d4f0595..0f37c174b 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -217,7 +217,9 @@ private void initScriptEditorTab(int yoffset) { } // Set the scripting language for proper syntax highlighting - activeArea.setLanguage(this.handler.getLanguage()); + // Use the container's language if available, otherwise fall back to handler's language + String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); + activeArea.setLanguage(language); // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); @@ -243,6 +245,13 @@ private void initScriptEditorTab(int yoffset) { this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); + + // Language toggle button (only if handler supports Janino) + if (handler.supportsJanino() && container != null) { + String langLabel = container.isJanino() ? "Java" : "ECMAScript"; + this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, langLabel)); + } + this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); @@ -538,6 +547,40 @@ protected void actionPerformed(GuiButton guibutton) { this.setSubGui(new EventGuiScriptList((List) this.languages.get(this.handler.getLanguage()), container)); } + + // Language toggle button - switch between ECMAScript and Java + if (guibutton.id == 113) { + int idx = getActiveScriptIndex(); + if (idx >= 0 && idx < handler.getScripts().size()) { + IScriptUnit currentUnit = handler.getScripts().get(idx); + IScriptUnit newUnit; + + if (currentUnit.isJanino()) { + // Switch from Java to ECMAScript + newUnit = new ScriptContainer(handler); + newUnit.setScript(currentUnit.getScript()); + newUnit.setExternalScripts(new ArrayList<>(currentUnit.getExternalScripts())); + } else { + // Switch from ECMAScript to Java + newUnit = handler.createJaninoScriptUnit(); + if (newUnit != null) { + newUnit.setScript(currentUnit.getScript()); + newUnit.setExternalScripts(new ArrayList<>(currentUnit.getExternalScripts())); + } else { + return; // Handler doesn't support Janino + } + } + + // Replace the script unit in the list + handler.getScripts().set(idx, newUnit); + + // Clear the cached text area so it recreates with new language + textAreas.remove(idx); + + // Reinitialize the GUI + initGui(); + } + } } protected void 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 4cedac26f..fceaeb6ed 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java @@ -6,13 +6,12 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomItems; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.scripted.item.ScriptCustomItem; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; +import java.util.*; public class GuiScriptItem extends GuiScriptInterface { private ScriptCustomItem item; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index 08e0cf2fd..6eb43722a 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -6,13 +6,17 @@ import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.client.gui.global.GuiNPCManageLinked; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.LinkedItemScript; +import java.util.ArrayList; + public class GuiScriptLinkedItem extends GuiScriptInterface { public final LinkedItem linkedItem; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index f1bda66dd..0b70a1233 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -6,13 +6,17 @@ import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.client.gui.global.GuiNpcManageRecipes; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; import noppes.npcs.controllers.data.RecipeScript; +import java.util.ArrayList; + public class GuiScriptRecipe extends GuiScriptInterface { public final int recipeId; diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index d89fbc7a2..2aa9c1db5 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -27,4 +27,23 @@ public interface IScriptHandler { Map getConsoleText(); void clearConsole(); + + /** + * Create a new Janino (Java) script unit for this handler. + * Handlers that don't support Janino scripts should return null. + * + * @return A new JaninoScript instance, or null if not supported + */ + default IScriptUnit createJaninoScriptUnit() { + return null; // Default: Janino not supported + } + + /** + * Check if this handler supports Janino (Java) scripts. + * + * @return true if createJaninoScriptUnit() can create valid Janino scripts + */ + default boolean supportsJanino() { + return createJaninoScriptUnit() != null; + } } diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 26a0eb89a..3d71e0023 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -59,22 +59,7 @@ import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.gui.ICustomGui; -import noppes.npcs.api.handler.IActionManager; -import noppes.npcs.api.handler.IAbilityHandler; -import noppes.npcs.api.handler.IAnimationHandler; -import noppes.npcs.api.handler.IAttributeHandler; -import noppes.npcs.api.handler.ICloneHandler; -import noppes.npcs.api.handler.ICustomEffectHandler; -import noppes.npcs.api.handler.IDialogHandler; -import noppes.npcs.api.handler.IFactionHandler; -import noppes.npcs.api.handler.IMagicHandler; -import noppes.npcs.api.handler.INaturalSpawnsHandler; -import noppes.npcs.api.handler.IPartyHandler; -import noppes.npcs.api.handler.IProfileHandler; -import noppes.npcs.api.handler.IQuestHandler; -import noppes.npcs.api.handler.IRecipeHandler; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.api.handler.ITransportHandler; +import noppes.npcs.api.handler.*; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; import noppes.npcs.api.handler.data.IFramePart; From 21e05575adb0921182ef5a7d549c7606e25bacb4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 00:30:25 +0200 Subject: [PATCH 274/337] merge conflicts --- .../client/gui/script/GuiScriptInterface.java | 14 ++++++-- .../client/gui/util/GuiScriptTextArea.java | 32 +------------------ 2 files changed, 13 insertions(+), 33 deletions(-) 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 3e16377f4..56a8c13d8 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -318,11 +318,21 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { - // Check fullscreen button first when on script editor tab - if (this.activeTab > 0 && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + // Check if click is within autocomplete menu bounds and consume it if so + GuiScriptTextArea activeArea = getActiveScriptArea(); + boolean isOverAutocomplete = activeArea != null + && activeArea.isPointOnAutocompleteMenu(mouseX, mouseY); + if (isOverAutocomplete) { + activeArea.mouseClicked(mouseX, mouseY, mouseButton); return; } + // Check fullscreen button first when on script editor tab + // BUT only if autocomplete is not visible (don't let clicks pass through autocomplete menu) + if (this.activeTab > 0 && !isOverAutocomplete + && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } super.mouseClicked(mouseX, mouseY, mouseButton); } 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 ce5912154..da73bcc2c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -559,15 +559,7 @@ public int[] getViewportDimensions() { public boolean fullscreen() { return GuiScriptInterface.isFullscreen; } - - public void setLanguage(String language) { - if (this.container != null) { - // this.container.setLanguage(language); - if (this.enableCodeHighlighting) { - this.container.formatCodeText(); - } - } - } + // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) @@ -2660,28 +2652,6 @@ public ScriptContext getScriptContext() { return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; } - /** - * 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; - return ScriptContext.GLOBAL; - } - public void setListener(ITextChangeListener listener) { this.listener = listener; } From 165e3e2d1444387689a41fc6ff414f8b21063d36 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 02:49:20 +0200 Subject: [PATCH 275/337] Passed default imports to JaninoScript --- .../java/noppes/npcs/janino/JaninoScript.java | 11 +++++---- .../npcs/janino/impl/JaninoNpcScript.java | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 0f9d5c9f1..22fb9ee95 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -58,11 +58,14 @@ public abstract class JaninoScript implements IScriptUnit { // Map from EnumScriptType to Method for OLD tab-based script system private Map hookTypeMap = new HashMap<>(); - protected JaninoScript(Class type, Consumer> buildSettings) { + // Default imports for this script type (e.g., noppes.npcs.api.*) + private final String[] defaultImports; + + protected JaninoScript(Class type, String[] defaultImports) { this.type = type; - this.builder = IScriptBodyBuilder - .getBuilder(type, CustomNpcs.getClientCompiler()); - buildSettings.accept(builder); + this.defaultImports = defaultImports != null ? defaultImports : new String[0]; + this.builder = IScriptBodyBuilder.getBuilder(type, CustomNpcs.getClientCompiler()) + .setDefaultImports(defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index b41e2aac8..4b051b37e 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -14,18 +14,19 @@ */ public class JaninoNpcScript extends JaninoScript { + /** Default imports available to NPC scripts without explicit import statements. */ + public static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.*", + "noppes.npcs.api.entity.*", + "noppes.npcs.api.event.*", + "noppes.npcs.api.handler.*", + "noppes.npcs.api.handler.data.*", + "noppes.npcs.api.item.*", + "noppes.npcs.api.ability.*" + }; + public JaninoNpcScript() { - super(Functions.class, (builder) -> builder - .setDefaultImports( - "noppes.npcs.api.*", - "noppes.npcs.api.entity.*", - "noppes.npcs.api.event.*", - "noppes.npcs.api.handler.*", - "noppes.npcs.api.handler.data.*", - "noppes.npcs.api.item.*", - "noppes.npcs.api.ability.*" - ) - ); + super(Functions.class, DEFAULT_IMPORTS); } /** From 26a9c99000faae3bd038fc0ddedff7dcd90b2e4c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 03:02:15 +0200 Subject: [PATCH 276/337] All Imports used in JaninoScript implementations (param types/return types) are now passed implicitly to ScriptDocument to store for when hook stubs are generated/hooks are manually added --- .../client/gui/script/GuiScriptInterface.java | 24 ++- .../client/gui/util/GuiScriptTextArea.java | 20 ++- .../script/interpreter/ScriptDocument.java | 148 +++++++++++++++++- .../interpreter/ScriptTextContainer.java | 19 ++- .../java/noppes/npcs/janino/JaninoScript.java | 52 ++++++ 5 files changed, 255 insertions(+), 8 deletions(-) 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 56a8c13d8..1f791cff2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,6 +27,7 @@ import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.janino.JaninoScript; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; @@ -38,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { protected int activeTab = 0; @@ -221,13 +223,28 @@ private void initScriptEditorTab(int yoffset) { // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); - activeArea.setLanguage(language); + activeArea.setLanguage(language); // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); + activeArea.enableCodeHighlighting(); - // Set the script context for context-aware hook autocomplete - activeArea.setScriptContext(getScriptContext()); + // For JaninoScripts, add implicit imports (default imports + hook signature types) + // These allow the syntax highlighter to resolve types without explicit import statements + if (container instanceof JaninoScript) { + JaninoScript janinoScript = (JaninoScript) container; + + // Add default imports (e.g., noppes.npcs.api.*, noppes.npcs.api.entity.*, etc.) + activeArea.addImplicitImports(janinoScript.getDefaultImports()); + + // Add hook types from signatures (parameters + return types) + // e.g., INpcEvent.InitEvent, Color, String, IOverlayContext, etc. + Set hookTypes = janinoScript.getHookTypes(); + activeArea.addImplicitImports(hookTypes.toArray(new String[0])); + + // Format to update which implicit imports are actually used + activeArea.formatCodeText(); + } // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { @@ -236,7 +253,6 @@ private void initScriptEditorTab(int yoffset) { } }); - activeArea.enableCodeHighlighting(); this.addTextField(activeArea); // ==================== FULLSCREEN BUTTON ==================== 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 da73bcc2c..c1b7b0918 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2652,6 +2652,18 @@ public ScriptContext getScriptContext() { return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; } + /** + * Add implicit imports that should be resolved without explicit import statements. + * Used for JaninoScript default imports and hook parameter types. + * + * @param patterns Array of import patterns to add (wildcard packages like "noppes.npcs.api.*" or FQ class names) + */ + public void addImplicitImports(String... patterns) { + if (this.container != null) { + this.container.addImplicitImports(patterns); + } + } + public void setListener(ITextChangeListener listener) { this.listener = listener; } @@ -2797,7 +2809,13 @@ public int compare(ImportEntry a, ImportEntry b) { selection.reset(savedCursorPos + cursorAdjustment); scrollToCursor(); } - + + public void formatCodeText() { + if (this.enableCodeHighlighting && this.container != null) { + this.container.formatCodeText(); + } + } + /** * Helper class for tracking imports during sorting. */ 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 d0be41eee..a74d473fa 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 @@ -130,6 +130,10 @@ public class ScriptDocument { // Determines which hooks and event types are available private ScriptContext scriptContext = ScriptContext.GLOBAL; + // Implicit imports from JaninoScript default imports and hook parameter types + // These are types that should be resolved even without explicit import statements + private final Set implicitImports = new HashSet<>(); + // Layout properties public int lineHeight = 13; public int totalHeight; @@ -194,6 +198,27 @@ public ScriptContext getScriptContext() { return scriptContext; } + /** + * Add implicit imports that should be resolved without explicit import statements. + * Used for JaninoScript default imports and hook parameter types. + * + * Patterns can be: + * - Wildcard: "noppes.npcs.api.*" (imports all classes from package) + * - Specific class: "noppes.npcs.api.event.INpcEvent$InitEvent" (nested class) + * - Regular class: "noppes.npcs.api.entity.IEntity" + * + * @param patterns Array of import patterns to add (packages with .* or fully qualified class names) + */ + public void addImplicitImports(String... patterns) { + if (patterns != null) { + for (String pattern : patterns) { + if (pattern != null && !pattern.isEmpty()) { + this.implicitImports.add(pattern); + } + } + } + } + // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { @@ -296,6 +321,9 @@ public void formatCodeText() { line.buildTokensFromMarks(marks, text, this); } + // Phase 6.5: Now that tokens are built, remove implicit imports that aren't actually used + removeUnusedImplicitImports(); + // Phase 7: Compute indent guides computeIndentGuides(marks); } @@ -451,10 +479,123 @@ private void parseImports() { } } - // Resolve all imports + // Add ALL implicit imports initially so tokens can be built with proper type resolution + addAllImplicitImportsToList(); + + // Resolve all imports (including all implicit ones) + importsBySimpleName = typeResolver.resolveImports(imports); + } + + /** + * Add ALL implicit imports to the imports list initially. + * This allows tokens to be built with proper type resolution. + * After tokenization, we'll remove the unused ones. + */ + private void addAllImplicitImportsToList() { + for (String pattern : implicitImports) { + if (pattern == null || pattern.isEmpty()) continue; + + boolean isWildcard = pattern.endsWith(".*"); + String fullPath; + String simpleName; + + if (isWildcard) { + fullPath = pattern.substring(0, pattern.length() - 2); // Remove ".*" + simpleName = null; + wildcardPackages.add(fullPath); + } else { + fullPath = pattern.replace('$', '.'); + int lastDot = fullPath.lastIndexOf('.'); + simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath; + } + + ImportData importData = new ImportData( + fullPath, simpleName, isWildcard, false, + -1, -1, -1, -1 + ); + + if (!imports.contains(importData)) { + imports.add(importData); + } + } + } + + /** + * Remove implicit imports that aren't actually used in the text. + * This keeps autocomplete clean by not showing types from hooks that aren't present. + * Called after tokenization is complete. + */ + private void removeUnusedImplicitImports() { + // Collect imports to remove (can't remove during iteration) + List toRemove = new ArrayList<>(); + + for (ImportData imp : imports) { + // Only check implicit imports (those with offset -1) + if (imp.getStartOffset() != -1) continue; + + // Skip wildcard imports - keep them all + if (imp.isWildcard()) continue; + + // Check if this specific class import is actually used + String simpleName = imp.getSimpleName(); + String fullPath = imp.getFullPath(); + + if (!isTypeReferenced(simpleName, fullPath)) { + toRemove.add(imp); + } + } + + // Remove unused implicit imports + imports.removeAll(toRemove); + + // Re-resolve imports after removing unused ones importsBySimpleName = typeResolver.resolveImports(imports); } + /** + * Check if a type is referenced anywhere in the script tokens. + * Looks for the simple name or any part of the fully qualified name. + */ + private boolean isTypeReferenced(String simpleName, String fullPath) { + if (simpleName == null || simpleName.isEmpty()) return false; + + // Check all lines and their tokens + for (ScriptLine line : lines) { + for (Token token : line.getTokens()) { + String tokenText = token.getText(); + + // Check if token matches the simple name + if (tokenText.equals(simpleName)) { + return true; + } + + // Check if token contains nested class reference (e.g., "InitEvent" in "INpcEvent.InitEvent") + if (fullPath.contains("$") && tokenText.equals(simpleName)) { + return true; + } + + // Check for partial matches in qualified references + if (fullPath.contains(".") && tokenText.contains(".")) { + // e.g., "INpcEvent.InitEvent" matches "noppes.npcs.api.event.INpcEvent$InitEvent" + String[] pathParts = fullPath.replace('$', '.').split("\\."); + String[] tokenParts = tokenText.split("\\."); + + // Check if the last parts match (e.g., "InitEvent") + if (pathParts.length > 0 && tokenParts.length > 0) { + String lastPathPart = pathParts[pathParts.length - 1]; + String lastTokenPart = tokenParts[tokenParts.length - 1]; + if (lastTokenPart.equals(lastPathPart)) { + return true; + } + } + } + } + } + + return false; + } + + // ==================== PHASE 3: STRUCTURE ==================== /** @@ -1934,6 +2075,11 @@ private void addPatternMarks(List marks, Pattern pattern, Token private void markImports(List marks) { for (ImportData imp : imports) { + // Skip implicit imports (from JaninoScript defaults, not in source text) + // These have offset -1 and don't need to be visually marked + if (imp.getStartOffset() < 0) + continue; + // Mark 'import' keyword marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.IMPORT_KEYWORD, imp)); 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 4836c0834..4b2e89e26 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 @@ -94,6 +94,18 @@ public ScriptContext getScriptContext() { return scriptContext; } + /** + * Add implicit imports that should be resolved without explicit import statements. + * Used for JaninoScript default imports and hook parameter types. + * + * @param patterns Array of import patterns to add (wildcard packages like "noppes.npcs.api.*" or FQ class names) + */ + public void addImplicitImports(String... patterns) { + if (document != null) { + document.addImplicitImports(patterns); + } + } + @Override public void init(int width, int height) { if (!USE_NEW_INTERPRETER) { @@ -127,9 +139,12 @@ public void init(String text, int width, int height) { super.init(text, width, height); return; } + + if (document == null) { + document = new ScriptDocument(this.text, this.language); + document.setScriptContext(this.scriptContext); + } - document = new ScriptDocument(this.text, this.language); - document.setScriptContext(this.scriptContext); init(width, height); } diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 22fb9ee95..d1066c329 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -466,6 +466,58 @@ public List getHookList() { return new ArrayList<>(hookList); } + /** + * Get the default imports configured for this script type. + * These are packages/classes that are automatically available without explicit import statements. + * + * @return Array of default import patterns (e.g., "noppes.npcs.api.*") + */ + public String[] getDefaultImports() { + return defaultImports; + } + + /** + * Get all types used in hook method signatures (parameters and return types). + * This includes event types like INpcEvent.InitEvent, INpcEvent.DamagedEvent, etc., + * as well as return types like Color, String, etc. + * Useful for syntax highlighting to know what types are implicitly available. + * + * @return Set of fully qualified class names for all hook parameter and return types + */ + public Set getHookTypes() { + Set types = new HashSet<>(); + for (Method method : type.getDeclaredMethods()) { + // Add parameter types + for (Class paramType : method.getParameterTypes()) + addTypeAndEnclosingTypes(types, paramType); + + // Add return types (in case any hook has a non-void return) + Class returnType = method.getReturnType(); + if (returnType != void.class) + addTypeAndEnclosingTypes(types, returnType); + } + return types; + } + + /** + * Add a type and all its enclosing types to the set. + * For nested classes like INpcEvent.InitEvent, this adds both INpcEvent and INpcEvent$InitEvent. + */ + private void addTypeAndEnclosingTypes(Set types, Class clazz) { + if (clazz == null || clazz.isPrimitive()) + return; + + // Add the type itself + types.add(clazz.getName()); + + // Add enclosing/declaring class if it's a nested type + Class enclosing = clazz.getDeclaringClass(); + while (enclosing != null) { + types.add(enclosing.getName()); + enclosing = enclosing.getDeclaringClass(); + } + } + @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { From 190fa5431f92942dde3993ef6e3db370d845a711 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 23 Jan 2026 20:11:26 +0200 Subject: [PATCH 277/337] HookDefinition requiredImports now has both enclosed type & enclosing type i.e "IPlayerEvent" & "IPlayerEvent.UpdateEvent"' Cached all collected imports in JaninoScript --- .../npcs/controllers/HookDefinition.java | 35 +++++++++++-------- .../java/noppes/npcs/janino/JaninoScript.java | 16 +++++++-- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java index 864e37ee3..be6c09397 100644 --- a/src/main/java/noppes/npcs/controllers/HookDefinition.java +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -148,9 +148,9 @@ public static HookDefinition fromMethod(String hookName, Method method) { } // Build required imports from event type - String importName = getImportForClass(eventType); - if (importName != null) { - builder.requiredImports(importName); + String[] importNames = getImportsForClass(eventType); + if (importNames.length > 0) { + builder.requiredImports(importNames); } } @@ -185,26 +185,31 @@ public static HookDefinition fromMethod(Method method) { } /** - * Get the import statement needed for a class. - * For nested classes, returns the enclosing class. + * Get the import statements needed for a class. + * For nested classes, includes both enclosing and nested names. */ - private static String getImportForClass(Class clazz) { + private static String[] getImportsForClass(Class clazz) { if (clazz == null || clazz.isPrimitive()) { - return null; + return new String[0]; + } + + String fullName = clazz.getName(); + if (fullName.startsWith("java.lang.")) { + return new String[0]; } - // For nested classes, get the top-level enclosing class Class enclosing = clazz; while (enclosing.getEnclosingClass() != null) { enclosing = enclosing.getEnclosingClass(); } - String name = enclosing.getName(); - if (name.startsWith("java.lang.")) { - return null; + String enclosingName = enclosing.getName(); + String nestedName = fullName.replace('$', '.'); + if (!nestedName.equals(enclosingName)) { + return new String[]{enclosingName, nestedName}; } - return name; + return new String[]{enclosingName}; } // ==================== Builder ==================== @@ -231,9 +236,9 @@ public Builder eventClass(Class clazz) { if (clazz != null) { this.eventClassName = clazz.getName(); - String importName = getImportForClass(clazz); - if (importName != null) { - this.requiredImports = new String[]{importName}; + String[] importNames = getImportsForClass(clazz); + if (importNames.length > 0) { + this.requiredImports = importNames; } if (clazz.isAnnotationPresent(Cancelable.class)) { diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 7c90016f5..8560cf4af 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -43,7 +43,9 @@ public abstract class JaninoScript implements IScriptUnit { private final JaninoHookResolver hookResolver = new JaninoHookResolver(); private final String[] defaultImports; - + // Cache of imports used in the last compilation + private String[] cachedImports; + private Map hookDefCache; private int lastHookRevision = -1; private int lastSeenGlobalRevision; @@ -122,8 +124,7 @@ public void unload() { public void compileScript(String code) { try { - String[] imports = collectImportsForCode(code); - builder.setDefaultImports(imports); + builder.setDefaultImports(cachedImports = collectImportsForCode(code)); this.scriptBody = builder.build(); scriptBody.setScript(code); } catch (InternalCompilerException e) { @@ -400,6 +401,13 @@ public String[] getDefaultImports() { return defaultImports; } + private String[] getCachedImports() { + if (cachedImports == null) { + cachedImports = collectImportsForCode(getFullCode()); + } + return cachedImports; + } + /** * Get all types used in hook method signatures (parameters and return types). * This includes event types like INpcEvent.InitEvent, INpcEvent.DamagedEvent, etc., @@ -453,6 +461,7 @@ public String getScript() { public void setScript(String script) { this.script = script; this.evaluated = false; + this.cachedImports = null; hookResolver.clearResolutionCaches(); } @@ -465,6 +474,7 @@ public List getExternalScripts() { public void setExternalScripts(List scripts) { this.externalScripts = scripts; this.evaluated = false; + this.cachedImports = null; hookResolver.clearResolutionCaches(); } From d3f06f886693246816ab55430322d304d7890330 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 23 Jan 2026 20:11:59 +0200 Subject: [PATCH 278/337] Reverted hook list scroll fix (cause it works fine without it) --- .../client/gui/util/GuiScriptTextArea.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 fd54724f3..c8e3af3de 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -598,16 +598,18 @@ public void drawTextBox(int xMouse, int yMouse) { int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); // Handle mouse wheel scroll - boolean isMouseOverTextArea = xMouse >= x && xMouse < x + width && yMouse >= y && yMouse < y + height; - int wheelDelta = 0; - if (isMouseOverTextArea) { - wheelDelta = Mouse.getDWheel(); - if (listener instanceof GuiNPCInterface) { - ((GuiNPCInterface) listener).mouseScroll = wheelDelta; - } - boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; - if (wheelDelta != 0 && canScroll) - scroll.applyWheelScroll(wheelDelta, maxScroll); + int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); + if (listener instanceof GuiNPCInterface) { + ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + + // Let autocomplete menu consume scroll first if visible + if (wheelDelta != 0 && autocompleteManager.isVisible() && autocompleteManager.mouseScrolled(xMouse, yMouse, wheelDelta)) { + // Autocomplete consumed the scroll + } else { + boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; + if (wheelDelta != 0 && canScroll) + scroll.applyWheelScroll(wheelDelta, maxScroll); + } } // Handle scrollbar dragging (delegated to ScrollState) From 6d62a44be5208a7847f9a4682b15ce655f0adb38 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 23 Jan 2026 20:12:18 +0200 Subject: [PATCH 279/337] getHookTypes is now cached imports based --- src/main/java/noppes/npcs/janino/JaninoScript.java | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8560cf4af..337cc9ae8 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -418,16 +418,7 @@ private String[] getCachedImports() { */ public Set getHookTypes() { Set types = new HashSet<>(); - for (Method method : type.getDeclaredMethods()) { - // Add parameter types - for (Class paramType : method.getParameterTypes()) - addTypeAndEnclosingTypes(types, paramType); - - // Add return types (in case any hook has a non-void return) - Class returnType = method.getReturnType(); - if (returnType != void.class) - addTypeAndEnclosingTypes(types, returnType); - } + Collections.addAll(types, getCachedImports()); return types; } From b7a46cab1d462fe1f82d563cad86302bfdc0684b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 23 Jan 2026 22:44:01 +0200 Subject: [PATCH 280/337] Added more JSDoc tags : @see, @deprecated, @since --- .../interpreter/hover/TokenHoverInfo.java | 44 ++++ .../interpreter/js_parser/DTSJSDocParser.java | 203 ++++++++++++++++++ .../interpreter/jsdoc/JSDocDeprecatedTag.java | 34 +++ .../script/interpreter/jsdoc/JSDocInfo.java | 30 +++ .../script/interpreter/jsdoc/JSDocSeeTag.java | 75 +++++++ .../interpreter/jsdoc/JSDocSinceTag.java | 30 +++ 6 files changed, 416 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSeeTag.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 40da28d3f..2a1cece89 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -5,10 +5,14 @@ import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocDeprecatedTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocSeeTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocSinceTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTypeTag; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; @@ -1193,6 +1197,46 @@ private void formatJSDocumentation(JSDocInfo jsDoc, List methodParams jsDocLines.add(returnLine); } + + JSDocDeprecatedTag deprecatedTag = jsDoc.getDeprecatedTag(); + if (deprecatedTag != null) { + DocumentationLine deprecatedLine = new DocumentationLine(); + deprecatedLine.addSegment("@deprecated", TokenType.JSDOC_TAG.getHexColor()); + + if (deprecatedTag.hasReason()) { + deprecatedLine.addText(" - "); + deprecatedLine.addText(deprecatedTag.getReason()); + } + + jsDocLines.add(deprecatedLine); + } + + JSDocSinceTag sinceTag = jsDoc.getSinceTag(); + if (sinceTag != null) { + DocumentationLine sinceLine = new DocumentationLine(); + sinceLine.addSegment("Since:", TokenType.JSDOC_TAG.getHexColor()); + sinceLine.addText(" " + sinceTag.getVersion()); + + jsDocLines.add(sinceLine); + } + + List seeTags = jsDoc.getSeeTags(); + if (seeTags != null && !seeTags.isEmpty()) { + for (JSDocSeeTag seeTag : seeTags) { + DocumentationLine seeLine = new DocumentationLine(); + seeLine.addSegment("See:", TokenType.JSDOC_TAG.getHexColor()); + + String reference = seeTag.getReference(); + if (seeTag.hasLinkText()) { + seeLine.addText(" "); + seeLine.addSegment(seeTag.getLinkText(), TokenType.INTERFACE_DECL.getHexColor()); + } else if (reference != null) { + seeLine.addText(" " + reference); + } + + jsDocLines.add(seeLine); + } + } } private void extractJavadoc(Method method) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java new file mode 100644 index 000000000..ecf41f240 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java @@ -0,0 +1,203 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DTSJSDocParser { + + private static final Pattern JSDOC_BLOCK_PATTERN = Pattern.compile( + "/\\*\\*\\s*(.*?)\\*/", Pattern.DOTALL); + + private static final Pattern TAG_PATTERN = Pattern.compile( + "@(\\w+)(?:\\s+(.*))?"); + + private static final Pattern PARAM_PATTERN = Pattern.compile( + "@param\\s+(?:\\{([^}]+)\\}\\s+)?(\\w+)(?:\\s+(.*))?"); + + private static final Pattern RETURN_PATTERN = Pattern.compile( + "@returns?\\s+(?:\\{([^}]+)\\}\\s*)?(.*)"); + + private static final Pattern TYPE_PATTERN = Pattern.compile( + "@type\\s+\\{([^}]+)\\}(?:\\s+(.*))?"); + + public static JSDocInfo parseJSDocBlock(String jsDocContent) { + if (jsDocContent == null || jsDocContent.isEmpty()) { + return null; + } + + JSDocInfo info = new JSDocInfo(jsDocContent, -1, -1); + + String[] lines = jsDocContent.split("\\r?\\n"); + StringBuilder descriptionBuilder = new StringBuilder(); + boolean foundFirstTag = false; + JSDocTag lastTag = null; + + for (String line : lines) { + line = cleanLine(line); + if (line.isEmpty()) continue; + + if (line.startsWith("@")) { + foundFirstTag = true; + lastTag = parseTag(line, info); + } else if (!foundFirstTag) { + if (descriptionBuilder.length() > 0) { + descriptionBuilder.append(" "); + } + descriptionBuilder.append(line); + } else if (lastTag != null) { + String existing = lastTag.getDescription(); + if (existing == null || existing.isEmpty()) { + lastTag.setDescription(line); + } else { + lastTag.setDescription(existing + " " + line); + } + } + } + + if (descriptionBuilder.length() > 0) { + info.setDescription(descriptionBuilder.toString().trim()); + } + + return info; + } + + private static String cleanLine(String line) { + line = line.trim(); + if (line.startsWith("/**")) { + line = line.substring(3).trim(); + } + if (line.endsWith("*/")) { + line = line.substring(0, line.length() - 2).trim(); + } + if (line.startsWith("*")) { + line = line.substring(1).trim(); + } + return line; + } + + private static JSDocTag parseTag(String line, JSDocInfo info) { + Matcher paramMatcher = PARAM_PATTERN.matcher(line); + if (paramMatcher.find()) { + String type = paramMatcher.group(1); + String name = paramMatcher.group(2); + String desc = paramMatcher.group(3); + JSDocParamTag tag = JSDocParamTag.create(-1, -1, -1, + type, null, -1, -1, name, -1, -1, desc != null ? desc.trim() : null); + info.addParamTag(tag); + return tag; + } + + Matcher returnMatcher = RETURN_PATTERN.matcher(line); + if (returnMatcher.find()) { + String type = returnMatcher.group(1); + String desc = returnMatcher.group(2); + if (desc != null) { + desc = desc.trim(); + if (desc.isEmpty()) { + desc = null; + } + } + JSDocReturnTag tag = JSDocReturnTag.create("return", -1, -1, -1, + type, null, -1, -1, desc); + info.setReturnTag(tag); + return tag; + } + + Matcher typeMatcher = TYPE_PATTERN.matcher(line); + if (typeMatcher.find()) { + String type = typeMatcher.group(1); + String desc = typeMatcher.group(2); + JSDocTypeTag tag = JSDocTypeTag.create(-1, -1, -1, + type, null, -1, -1, desc != null ? desc.trim() : null); + info.setTypeTag(tag); + return tag; + } + + Matcher tagMatcher = TAG_PATTERN.matcher(line); + if (tagMatcher.find()) { + String tagName = tagMatcher.group(1); + String rest = tagMatcher.group(2); + + switch (tagName) { + case "since": + JSDocSinceTag sinceTag = JSDocSinceTag.createSimple(rest != null ? rest.trim() : ""); + info.setSinceTag(sinceTag); + return sinceTag; + case "deprecated": + JSDocDeprecatedTag deprecatedTag = JSDocDeprecatedTag.createSimple(rest != null ? rest.trim() : ""); + info.setDeprecatedTag(deprecatedTag); + return deprecatedTag; + case "see": + JSDocSeeTag seeTag = JSDocSeeTag.createSimple(rest != null ? rest.trim() : ""); + info.addSeeTag(seeTag); + return seeTag; + default: + JSDocTag genericTag = new JSDocTag(tagName, -1, -1, -1); + genericTag.setDescription(rest != null ? rest.trim() : ""); + info.addTag(genericTag); + return genericTag; + } + } + return null; + } + + public static String extractJSDocBefore(String content, int elementStart) { + if (elementStart <= 0) return null; + + int searchStart = Math.max(0, elementStart - 2000); + String searchArea = content.substring(searchStart, elementStart); + + int lastJSDocEnd = searchArea.lastIndexOf("*/"); + if (lastJSDocEnd < 0) return null; + + int jsDocStart = searchArea.lastIndexOf("/**", lastJSDocEnd); + if (jsDocStart < 0) return null; + + String between = searchArea.substring(lastJSDocEnd + 2).trim(); + if (between.isEmpty() || isOnlyWhitespaceOrModifiers(between)) { + return searchArea.substring(jsDocStart, lastJSDocEnd + 2); + } + + return null; + } + + private static boolean isOnlyWhitespaceOrModifiers(String text) { + String cleaned = text.replaceAll("\\s+", " ").trim(); + return cleaned.isEmpty() + || cleaned.matches("^(export\\s*)?(readonly\\s*)?(interface|class|type|function)?\\s*$"); + } + + public static List extractAllJSDocBlocks(String content) { + List blocks = new ArrayList<>(); + Matcher matcher = JSDOC_BLOCK_PATTERN.matcher(content); + + while (matcher.find()) { + int start = matcher.start(); + int end = matcher.end(); + String docContent = matcher.group(0); + blocks.add(new JSDocBlock(start, end, docContent)); + } + + return blocks; + } + + public static class JSDocBlock { + public final int start; + public final int end; + public final String content; + + public JSDocBlock(int start, int end, String content) { + this.start = start; + this.end = end; + this.content = content; + } + + public JSDocInfo parse() { + return parseJSDocBlock(content); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java new file mode 100644 index 000000000..f5f32d503 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java @@ -0,0 +1,34 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +public class JSDocDeprecatedTag extends JSDocTag { + + private final String reason; + + public JSDocDeprecatedTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String reason) { + super(tagName, atSignOffset, tagNameStart, tagNameEnd); + this.reason = reason; + this.setDescription(reason); + } + + public static JSDocDeprecatedTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, + String reason) { + return new JSDocDeprecatedTag(tagName, atSignOffset, tagNameStart, tagNameEnd, reason); + } + + public static JSDocDeprecatedTag createSimple(String reason) { + return new JSDocDeprecatedTag("deprecated", -1, -1, -1, reason); + } + + public String getReason() { + return reason; + } + + public boolean hasReason() { + return reason != null && !reason.isEmpty(); + } + + @Override + public String toString() { + return "JSDocDeprecatedTag{@deprecated" + (hasReason() ? " " + reason : "") + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java index 1cd0050b4..9591ca958 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java @@ -19,6 +19,9 @@ public class JSDocInfo { private JSDocTypeTag typeTag; private final List paramTags = new ArrayList<>(); private JSDocReturnTag returnTag; + private JSDocDeprecatedTag deprecatedTag; + private JSDocSinceTag sinceTag; + private final List seeTags = new ArrayList<>(); private String description; private final List allTags = new ArrayList<>(); @@ -43,6 +46,21 @@ public void setReturnTag(JSDocReturnTag returnTag) { allTags.add(returnTag); } + public void setDeprecatedTag(JSDocDeprecatedTag deprecatedTag) { + this.deprecatedTag = deprecatedTag; + allTags.add(deprecatedTag); + } + + public void setSinceTag(JSDocSinceTag sinceTag) { + this.sinceTag = sinceTag; + allTags.add(sinceTag); + } + + public void addSeeTag(JSDocSeeTag seeTag) { + this.seeTags.add(seeTag); + allTags.add(seeTag); + } + public void setDescription(String description) { this.description = description; } @@ -83,6 +101,15 @@ public TypeInfo getReturnType() { return returnTag != null ? returnTag.getTypeInfo() : null; } + public JSDocDeprecatedTag getDeprecatedTag() { return deprecatedTag; } + public boolean isDeprecated() { return deprecatedTag != null; } + + public JSDocSinceTag getSinceTag() { return sinceTag; } + public boolean hasSince() { return sinceTag != null; } + + public List getSeeTags() { return Collections.unmodifiableList(seeTags); } + public boolean hasSeeTags() { return !seeTags.isEmpty(); } + public String getDescription() { return description; } public List getAllTags() { return Collections.unmodifiableList(allTags); } @@ -92,6 +119,9 @@ public String toString() { if (typeTag != null) sb.append("type=").append(typeTag.getTypeName()); if (!paramTags.isEmpty()) sb.append(", params=").append(paramTags.size()); if (returnTag != null) sb.append(", return=").append(returnTag.getTypeName()); + if (deprecatedTag != null) sb.append(", deprecated"); + if (sinceTag != null) sb.append(", since=").append(sinceTag.getVersion()); + if (!seeTags.isEmpty()) sb.append(", see=").append(seeTags.size()); sb.append("}"); return sb.toString(); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSeeTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSeeTag.java new file mode 100644 index 000000000..028023c6e --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSeeTag.java @@ -0,0 +1,75 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +public class JSDocSeeTag extends JSDocTag { + + private final String reference; + private String url; + private String linkText; + + public JSDocSeeTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String reference) { + super(tagName, atSignOffset, tagNameStart, tagNameEnd); + this.reference = reference; + this.setDescription(reference); + parseReference(reference); + } + + public static JSDocSeeTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, + String reference) { + return new JSDocSeeTag(tagName, atSignOffset, tagNameStart, tagNameEnd, reference); + } + + public static JSDocSeeTag createSimple(String reference) { + return new JSDocSeeTag("see", -1, -1, -1, reference); + } + + private void parseReference(String ref) { + if (ref == null) return; + + if (ref.contains(""); + int textEnd = ref.indexOf(""); + if (textStart != -1 && textEnd != -1 && textStart < textEnd) { + this.linkText = ref.substring(textStart + 1, textEnd); + } + } else if (ref.contains("{@link")) { + int linkStart = ref.indexOf("{@link"); + int linkEnd = ref.indexOf("}", linkStart); + if (linkStart != -1 && linkEnd != -1) { + this.linkText = ref.substring(linkStart + 6, linkEnd).trim(); + } + } + } + + public String getReference() { + return reference; + } + + public String getUrl() { + return url; + } + + public boolean hasUrl() { + return url != null && !url.isEmpty(); + } + + public String getLinkText() { + return linkText; + } + + public boolean hasLinkText() { + return linkText != null && !linkText.isEmpty(); + } + + @Override + public String toString() { + return "JSDocSeeTag{@see " + reference + "}"; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java new file mode 100644 index 000000000..6c9005b0b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java @@ -0,0 +1,30 @@ +package noppes.npcs.client.gui.util.script.interpreter.jsdoc; + +public class JSDocSinceTag extends JSDocTag { + + private final String version; + + public JSDocSinceTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String version) { + super(tagName, atSignOffset, tagNameStart, tagNameEnd); + this.version = version; + this.setDescription(version); + } + + public static JSDocSinceTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, + String version) { + return new JSDocSinceTag(tagName, atSignOffset, tagNameStart, tagNameEnd, version); + } + + public static JSDocSinceTag createSimple(String version) { + return new JSDocSinceTag("since", -1, -1, -1, version); + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return "JSDocSinceTag{@since " + version + "}"; + } +} From ff68dc92584588534b5446ce328349d14451db91 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 23 Jan 2026 23:10:26 +0200 Subject: [PATCH 281/337] Parsed all JSDoc info for all members of all .d.ts files. Documentations for all API classes now renders properly in hover info -Added proper TokenHoverInfo for JS classes --- .../script/interpreter/field/FieldInfo.java | 8 ++- .../interpreter/hover/TokenHoverInfo.java | 17 ++++++ .../interpreter/js_parser/JSFieldInfo.java | 21 +++++-- .../interpreter/js_parser/JSMethodInfo.java | 21 +++++-- .../interpreter/js_parser/JSTypeInfo.java | 55 +++++++++++++++++-- .../interpreter/js_parser/JSTypeRegistry.java | 10 ++++ .../js_parser/TypeScriptDefinitionParser.java | 37 +++++++++++++ .../script/interpreter/method/MethodInfo.java | 11 +++- .../script/interpreter/type/TypeInfo.java | 5 ++ 9 files changed, 164 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index ec3958f02..55834d311 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -182,9 +182,13 @@ public static FieldInfo fromJSField(JSFieldInfo jsField, TypeInfo containingType } // Use documentation if available - String documentation = jsField.getDocumentation(); + JSDocInfo jsDocInfo = jsField.getJsDocInfo(); + String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null; + String documentation = jsDocDesc != null ? jsDocDesc : jsField.getDocumentation(); - return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null); + FieldInfo fieldInfo = new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null); + fieldInfo.setJSDocInfo(jsDocInfo); + return fieldInfo; } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 2a1cece89..216a9102d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -666,6 +666,23 @@ private void extractClassInfo(Token token) { } } + } else if (typeInfo.isJSType()) { + JSTypeInfo jsType = typeInfo.getJSTypeInfo(); + + iconIndicator = "I"; + addSegment("interface ", TokenType.MODIFIER.getHexColor()); + addSegment(getName(typeInfo), TokenType.INTERFACE_DECL.getHexColor()); + + if (jsType.getExtendsType() != null) { + addSegment(" extends ", TokenType.MODIFIER.getHexColor()); + addSegment(jsType.getExtendsType(), TokenType.INTERFACE_DECL.getHexColor()); + } + + JSDocInfo jsDocInfo = typeInfo.getJSDocInfo(); + if (jsDocInfo != null) { + formatJSDocumentation(jsDocInfo, null); + } + } else { // Unresolved type iconIndicator = "?"; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java index fc1ccd642..afcb09a6b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; /** @@ -11,7 +12,7 @@ public class JSFieldInfo { private final String type; // Raw type string for display private TypeInfo typeInfo; // Resolved TypeInfo private final boolean readonly; - private String documentation; + private JSDocInfo jsDocInfo; private JSTypeInfo containingType; // The type that contains this field public JSFieldInfo(String name, String type, boolean readonly) { @@ -20,8 +21,8 @@ public JSFieldInfo(String name, String type, boolean readonly) { this.readonly = readonly; } - public JSFieldInfo setDocumentation(String documentation) { - this.documentation = documentation; + public JSFieldInfo setJsDocInfo(JSDocInfo jsDocInfo) { + this.jsDocInfo = jsDocInfo; return this; } @@ -66,9 +67,17 @@ public TypeInfo getResolvedType(TypeInfo contextType) { public String getType() { return type; } public TypeInfo getTypeInfo() { return typeInfo; } public boolean isReadonly() { return readonly; } - public String getDocumentation() { return documentation; } + public JSDocInfo getJsDocInfo() { return jsDocInfo; } public JSTypeInfo getContainingType() { return containingType; } + /** + * Get documentation string (backward compatibility). + * Extracts description from JSDocInfo if available. + */ + public String getDocumentation() { + return jsDocInfo != null ? jsDocInfo.getDescription() : null; + } + /** * Get display name - uses resolved TypeInfo simple name if available. */ @@ -89,8 +98,8 @@ public String buildHoverInfo() { } sb.append("").append(name).append(": ").append(type).append(""); - if (documentation != null && !documentation.isEmpty()) { - sb.append("

").append(documentation); + if (jsDocInfo != null && jsDocInfo.getDescription() != null && !jsDocInfo.getDescription().isEmpty()) { + sb.append("

").append(jsDocInfo.getDescription()); } return sb.toString(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java index 8999582de..6aef02b7f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; @@ -14,7 +15,7 @@ public class JSMethodInfo { private final String returnType; // Raw type string for display, e.g., "number", "IEntity", "void" private TypeInfo returnTypeInfo; // Resolved TypeInfo (set during Phase 2) private final List parameters; - private String documentation; + private JSDocInfo jsDocInfo; private JSTypeInfo containingType; // The type that contains this method public JSMethodInfo(String name, String returnType, List parameters) { @@ -23,8 +24,8 @@ public JSMethodInfo(String name, String returnType, List parame this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>(); } - public JSMethodInfo setDocumentation(String documentation) { - this.documentation = documentation; + public JSMethodInfo setJsDocInfo(JSDocInfo jsDocInfo) { + this.jsDocInfo = jsDocInfo; return this; } @@ -74,10 +75,18 @@ public TypeInfo getResolvedReturnType(TypeInfo contextType) { public String getReturnType() { return returnType; } public TypeInfo getReturnTypeInfo() { return returnTypeInfo; } public List getParameters() { return parameters; } - public String getDocumentation() { return documentation; } + public JSDocInfo getJsDocInfo() { return jsDocInfo; } public int getParameterCount() { return parameters.size(); } public JSTypeInfo getContainingType() { return containingType; } + /** + * Get documentation string (backward compatibility). + * Extracts description from JSDocInfo if available. + */ + public String getDocumentation() { + return jsDocInfo != null ? jsDocInfo.getDescription() : null; + } + /** * Get a formatted signature string for display. */ @@ -106,8 +115,8 @@ public String buildHoverInfo() { } sb.append("): ").append(returnType).append(""); - if (documentation != null && !documentation.isEmpty()) { - sb.append("

").append(documentation); + if (jsDocInfo != null && jsDocInfo.getDescription() != null && !jsDocInfo.getDescription().isEmpty()) { + sb.append("

").append(jsDocInfo.getDescription()); } return sb.toString(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index f25e63a6b..8bdab8493 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -1,5 +1,9 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTypeTag; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; @@ -31,7 +35,7 @@ public class JSTypeInfo { private JSTypeInfo parentType; // The containing type (for inner types) // Documentation - private String documentation; + private JSDocInfo jsDocInfo; public JSTypeInfo(String simpleName, String namespace) { this.simpleName = simpleName; @@ -45,8 +49,8 @@ public JSTypeInfo setExtends(String extendsType) { return this; } - public JSTypeInfo setDocumentation(String documentation) { - this.documentation = documentation; + public JSTypeInfo setJsDocInfo(JSDocInfo jsDocInfo) { + this.jsDocInfo = jsDocInfo; return this; } @@ -86,7 +90,7 @@ public void setResolvedParent(JSTypeInfo parent) { public String getNamespace() { return namespace; } public String getExtendsType() { return extendsType; } public JSTypeInfo getResolvedParent() { return resolvedParent; } - public String getDocumentation() { return documentation; } + public JSDocInfo getJsDocInfo() { return jsDocInfo; } public JSTypeInfo getParentType() { return parentType; } public List getTypeParams() { return typeParams; } @@ -148,6 +152,49 @@ public void resolveMemberTypes() { } } + public void resolveJSDocTypes() { + TypeResolver resolver = TypeResolver.getInstance(); + + if (jsDocInfo != null) { + resolveJSDocInfoTypes(jsDocInfo, resolver); + } + + for (JSMethodInfo method : methods.values()) { + JSDocInfo methodDoc = method.getJsDocInfo(); + if (methodDoc != null) { + resolveJSDocInfoTypes(methodDoc, resolver); + } + } + + for (JSFieldInfo field : fields.values()) { + JSDocInfo fieldDoc = field.getJsDocInfo(); + if (fieldDoc != null) { + resolveJSDocInfoTypes(fieldDoc, resolver); + } + } + } + + private void resolveJSDocInfoTypes(JSDocInfo jsDoc, TypeResolver resolver) { + JSDocTypeTag typeTag = jsDoc.getTypeTag(); + if (typeTag != null && typeTag.hasType() && typeTag.getTypeInfo() == null) { + TypeInfo resolved = resolver.resolveJSType(typeTag.getTypeName()); + typeTag.setType(typeTag.getTypeName(), resolved, typeTag.getTypeStart(), typeTag.getTypeEnd()); + } + + for (JSDocParamTag paramTag : jsDoc.getParamTags()) { + if (paramTag.hasType() && paramTag.getTypeInfo() == null) { + TypeInfo resolved = resolver.resolveJSType(paramTag.getTypeName()); + paramTag.setType(paramTag.getTypeName(), resolved, paramTag.getTypeStart(), paramTag.getTypeEnd()); + } + } + + JSDocReturnTag returnTag = jsDoc.getReturnTag(); + if (returnTag != null && returnTag.hasType() && returnTag.getTypeInfo() == null) { + TypeInfo resolved = resolver.resolveJSType(returnTag.getTypeName()); + returnTag.setType(returnTag.getTypeName(), resolved, returnTag.getTypeStart(), returnTag.getTypeEnd()); + } + } + /** * Resolves a type parameter to its bound TypeInfo. * For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("T") returns the TypeInfo for EntityPlayerMP. 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 680172f99..89624c1d4 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 @@ -97,6 +97,9 @@ public void initializeFromResources() { // Phase 2b: Resolve member types (return types, field types, param types) resolveAllMemberTypes(); + // Phase 2c: Resolve JSDoc types (@param, @return, @type type references) + resolveAllJSDocTypes(); + resolveInheritance(); registerEngineGlobalObjects(); @@ -239,6 +242,7 @@ public void initializeFromDirectory(File directory) { parser.parseDirectory(directory); resolveAllTypeParameters(); resolveAllMemberTypes(); + resolveAllJSDocTypes(); resolveInheritance(); initialized = true; System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks"); @@ -259,6 +263,7 @@ public void initializeFromVsix(File vsixFile) { parser.parseVsixArchive(vsixFile); resolveAllTypeParameters(); resolveAllMemberTypes(); + resolveAllJSDocTypes(); resolveInheritance(); initialized = true; System.out.println("[JSTypeRegistry] Loaded " + types.size() + " types, " + hooks.size() + " hooks from VSIX"); @@ -594,6 +599,11 @@ public void resolveAllMemberTypes() { type.resolveMemberTypes(); } + public void resolveAllJSDocTypes() { + for (JSTypeInfo type : types.values()) + type.resolveJSDocTypes(); + } + /** * Resolve inheritance relationships between types. * For each type, walks up the parent chain and resolves all ancestors. 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 3e0d8b440..32b832ba9 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 @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; + import java.io.*; import java.util.*; import java.util.regex.*; @@ -216,6 +218,11 @@ private void parseInterfaceFile(String content, String parentNamespace) { JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace); + JSDocInfo jsDoc = extractJSDocBefore(content, interfaceMatcher.start()); + if (jsDoc != null) { + typeInfo.setJsDocInfo(jsDoc); + } + // Parse type parameters if (typeParamsStr != null && !typeParamsStr.isEmpty()) { parseTypeParameters(typeParamsStr, typeInfo); @@ -261,6 +268,11 @@ private void parseInterfaceFile(String content, String parentNamespace) { JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace); + JSDocInfo jsDoc = extractJSDocBefore(content, nestedInterfaceMatcher.start()); + if (jsDoc != null) { + typeInfo.setJsDocInfo(jsDoc); + } + // Parse type parameters if (typeParamsStr != null && !typeParamsStr.isEmpty()) { parseTypeParameters(typeParamsStr, typeInfo); @@ -296,6 +308,11 @@ private void parseInterfaceFile(String content, String parentNamespace) { JSTypeInfo typeInfo = new JSTypeInfo(className, parentNamespace); + JSDocInfo jsDoc = extractJSDocBefore(content, classMatcher.start()); + if (jsDoc != null) { + typeInfo.setJsDocInfo(jsDoc); + } + // Parse type parameters if (typeParamsStr != null && !typeParamsStr.isEmpty()) { parseTypeParameters(typeParamsStr, typeInfo); @@ -445,6 +462,12 @@ private void parseInterfaceBody(String body, JSTypeInfo typeInfo) { List parameters = parseParameters(params); JSMethodInfo method = new JSMethodInfo(methodName, returnType, parameters); + + JSDocInfo jsDoc = extractJSDocBefore(body, methodMatcher.start()); + if (jsDoc != null) { + method.setJsDocInfo(jsDoc); + } + typeInfo.addMethod(method); } @@ -460,6 +483,12 @@ private void parseInterfaceBody(String body, JSTypeInfo typeInfo) { String fieldType = cleanType(fieldMatcher.group(3)); JSFieldInfo field = new JSFieldInfo(fieldName, fieldType, readonly); + + JSDocInfo jsDoc = extractJSDocBefore(body, fieldMatcher.start()); + if (jsDoc != null) { + field.setJsDocInfo(jsDoc); + } + typeInfo.addField(field); } } @@ -568,4 +597,12 @@ private String readFully(BufferedReader reader) throws IOException { } return sb.toString(); } + + private JSDocInfo extractJSDocBefore(String content, int elementStart) { + String jsDocBlock = DTSJSDocParser.extractJSDocBefore(content, elementStart); + if (jsDocBlock != null) { + return DTSJSDocParser.parseJSDocBlock(jsDocBlock); + } + return null; + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index ababd9816..859d3c0e9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -205,9 +205,14 @@ public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containing int modifiers = Modifier.PUBLIC; // Use the documentation from the method if available - String documentation = jsMethod.getDocumentation(); - - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, null); + JSDocInfo jsDocInfo = jsMethod.getJsDocInfo(); + String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null; + String documentation = jsDocDesc != null ? jsDocDesc : jsMethod.getDocumentation(); + + MethodInfo methodInfo = new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, + false, modifiers, documentation, null); + methodInfo.setJSDocInfo(jsDocInfo); + return methodInfo; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 14f4c6242..cb0800ebb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -6,6 +6,7 @@ import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; @@ -227,6 +228,10 @@ public JSTypeInfo getJSTypeInfo() { return jsTypeInfo; } + public JSDocInfo getJSDocInfo() { + return jsTypeInfo != null ? jsTypeInfo.getJsDocInfo() : null; + } + // Getters public String getSimpleName() { return simpleName; } public String getFullName() { return fullName; } From 8799e08c17d5d54c71353b8cbdac5f4ec45d9e96 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 02:23:59 +0200 Subject: [PATCH 282/337] Expanded scope of DTSJSDocParser type/param patterns so they are more forgiving --- .../interpreter/hover/TokenHoverInfo.java | 10 +++++--- .../interpreter/js_parser/DTSJSDocParser.java | 24 +++++++++++++++---- .../script/interpreter/jsdoc/JSDocInfo.java | 3 ++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 216a9102d..b6a1ebbdc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -1166,10 +1166,14 @@ private void formatJSDocumentation(JSDocInfo jsDoc, List methodParams // Indent and parameter name String paramName = paramTag.getParamName(); - boolean paramExists = methodParams != null && methodParams.stream() + if (paramName == null || paramName.isEmpty()) { + paramLine.addSegment("param", TokenType.JSDOC_TAG.getHexColor()); + } else { + boolean paramExists = methodParams != null && methodParams.stream() .anyMatch(p -> p.getName().equals(paramName)); - paramLine.addSegment(paramName, - paramExists ? TokenType.PARAMETER.getHexColor() : TokenType.UNDEFINED_VAR.getHexColor()); + paramLine.addSegment(paramName, + paramExists ? TokenType.PARAMETER.getHexColor() : TokenType.UNDEFINED_VAR.getHexColor()); + } // Type if available if (paramTag.hasType()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java index ecf41f240..e3c1dd8bb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java @@ -16,13 +16,13 @@ public class DTSJSDocParser { "@(\\w+)(?:\\s+(.*))?"); private static final Pattern PARAM_PATTERN = Pattern.compile( - "@param\\s+(?:\\{([^}]+)\\}\\s+)?(\\w+)(?:\\s+(.*))?"); + "@param(?:\\s+\\{([^}]+)\\})?(?:\\s+(\\w+))?(?:\\s+(.*))?"); private static final Pattern RETURN_PATTERN = Pattern.compile( "@returns?\\s+(?:\\{([^}]+)\\}\\s*)?(.*)"); private static final Pattern TYPE_PATTERN = Pattern.compile( - "@type\\s+\\{([^}]+)\\}(?:\\s+(.*))?"); + "@type(?:\\s+\\{([^}]+)\\})?(?:\\s+(.*))?"); public static JSDocInfo parseJSDocBlock(String jsDocContent) { if (jsDocContent == null || jsDocContent.isEmpty()) { @@ -85,8 +85,18 @@ private static JSDocTag parseTag(String line, JSDocInfo info) { String type = paramMatcher.group(1); String name = paramMatcher.group(2); String desc = paramMatcher.group(3); + if (desc == null && type == null && name != null) { + desc = name; + name = null; + } + if (desc != null) { + desc = desc.trim(); + if (desc.isEmpty()) { + desc = null; + } + } JSDocParamTag tag = JSDocParamTag.create(-1, -1, -1, - type, null, -1, -1, name, -1, -1, desc != null ? desc.trim() : null); + type, null, -1, -1, name, -1, -1, desc); info.addParamTag(tag); return tag; } @@ -111,8 +121,14 @@ private static JSDocTag parseTag(String line, JSDocInfo info) { if (typeMatcher.find()) { String type = typeMatcher.group(1); String desc = typeMatcher.group(2); + if (desc != null) { + desc = desc.trim(); + if (desc.isEmpty()) { + desc = null; + } + } JSDocTypeTag tag = JSDocTypeTag.create(-1, -1, -1, - type, null, -1, -1, desc != null ? desc.trim() : null); + type, null, -1, -1, desc); info.setTypeTag(tag); return tag; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java index 9591ca958..b497da434 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java @@ -87,7 +87,8 @@ public List getParamTags() { public JSDocParamTag getParamTag(String paramName) { for (JSDocParamTag tag : paramTags) { - if (tag.getParamName().equals(paramName)) { + String tagName = tag.getParamName(); + if (tagName != null && tagName.equals(paramName)) { return tag; } } From 021a32b96209c6fe7ff01d40fad4ac28edc893c3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 02:24:13 +0200 Subject: [PATCH 283/337] Added JSDocInfo for TypeInfo too --- .../gui/util/script/interpreter/ScriptDocument.java | 6 ++++++ .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 5 +++++ .../client/gui/util/script/interpreter/type/TypeInfo.java | 8 ++++++++ 3 files changed, 19 insertions(+) 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 a74d473fa..5a5e29ef6 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 @@ -671,10 +671,16 @@ private void parseScriptTypes() { // Extract modifiers from the matched text String fullMatch = text.substring(m.start(), m.end()); int modifiers = parseModifiers(fullMatch); + + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, m.start()); ScriptTypeInfo scriptType = ScriptTypeInfo.create( typeName, kind, m.start(), bodyStart, bodyEnd, modifiers); + if (jsDoc != null) { + scriptType.setJSDocInfo(jsDoc); + } + // Store the script type first so it can be resolved by other types scriptTypes.put(typeName, scriptType); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index b6a1ebbdc..d3fa8d020 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -665,6 +665,11 @@ private void extractClassInfo(Token token) { errors.add(scriptType.getErrorMessage()); } } + + JSDocInfo jsDocInfo = scriptType.getJSDocInfo(); + if (jsDocInfo != null) { + formatJSDocumentation(jsDocInfo, null); + } } else if (typeInfo.isJSType()) { JSTypeInfo jsType = typeInfo.getJSTypeInfo(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index cb0800ebb..500c6afa2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -66,6 +66,9 @@ public enum Kind { // Type parameters (generics) private final List typeParams = new ArrayList<>(); + // Documentation (script-defined types) + private JSDocInfo jsDocInfo; + private TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { this(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, null); @@ -229,9 +232,14 @@ public JSTypeInfo getJSTypeInfo() { } public JSDocInfo getJSDocInfo() { + if (jsDocInfo != null) { + return jsDocInfo; + } return jsTypeInfo != null ? jsTypeInfo.getJsDocInfo() : null; } + public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; } + // Getters public String getSimpleName() { return simpleName; } public String getFullName() { return fullName; } From 1b9e674b368b36df35606fe7011141530bda04e4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 03:29:48 +0200 Subject: [PATCH 284/337] Proper parsing for generic JSDoc tags like "@word this is a tag" Removed deprecated/since tags, parsed them as generics instead --- .../interpreter/hover/TokenHoverInfo.java | 63 +++-- .../interpreter/js_parser/DTSJSDocParser.java | 17 +- .../interpreter/jsdoc/JSDocDeprecatedTag.java | 34 --- .../script/interpreter/jsdoc/JSDocInfo.java | 19 -- .../script/interpreter/jsdoc/JSDocParser.java | 253 +++++++++++++----- .../interpreter/jsdoc/JSDocSinceTag.java | 30 --- 6 files changed, 226 insertions(+), 190 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java delete mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index d3fa8d020..40963b165 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -5,12 +5,11 @@ import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; -import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocDeprecatedTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocSeeTag; -import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocSinceTag; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTag; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTypeTag; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -1224,28 +1223,6 @@ private void formatJSDocumentation(JSDocInfo jsDoc, List methodParams jsDocLines.add(returnLine); } - JSDocDeprecatedTag deprecatedTag = jsDoc.getDeprecatedTag(); - if (deprecatedTag != null) { - DocumentationLine deprecatedLine = new DocumentationLine(); - deprecatedLine.addSegment("@deprecated", TokenType.JSDOC_TAG.getHexColor()); - - if (deprecatedTag.hasReason()) { - deprecatedLine.addText(" - "); - deprecatedLine.addText(deprecatedTag.getReason()); - } - - jsDocLines.add(deprecatedLine); - } - - JSDocSinceTag sinceTag = jsDoc.getSinceTag(); - if (sinceTag != null) { - DocumentationLine sinceLine = new DocumentationLine(); - sinceLine.addSegment("Since:", TokenType.JSDOC_TAG.getHexColor()); - sinceLine.addText(" " + sinceTag.getVersion()); - - jsDocLines.add(sinceLine); - } - List seeTags = jsDoc.getSeeTags(); if (seeTags != null && !seeTags.isEmpty()) { for (JSDocSeeTag seeTag : seeTags) { @@ -1263,6 +1240,44 @@ private void formatJSDocumentation(JSDocInfo jsDoc, List methodParams jsDocLines.add(seeLine); } } + + List allTags = jsDoc.getAllTags(); + if (allTags != null && !allTags.isEmpty()) { + for (JSDocTag tag : allTags) { + String tagName = tag.getTagName(); + if (tagName == null) { + continue; + } + + String normalized = tagName.toLowerCase(); + if ("type".equals(normalized) + || "param".equals(normalized) + || "return".equals(normalized) + || "returns".equals(normalized) + || "see".equals(normalized)) { + continue; + } + + DocumentationLine tagLine = new DocumentationLine(); + //capitalise first letter in tag name + String capTagName = tagName.substring(0, 1).toUpperCase() + tagName.substring(1); + tagLine.addSegment(capTagName + ":", TokenType.JSDOC_TAG.getHexColor()); + + if (tag.hasType()) { + tagLine.addSegment(" {", TokenType.JSDOC_TYPE.getHexColor()); + tagLine.addSegment(tag.getTypeName(), TokenType.getColor(tag.getTypeInfo())); + tagLine.addSegment("}", TokenType.JSDOC_TYPE.getHexColor()); + } + + String tagDesc = tag.getDescription(); + if (tagDesc != null && !tagDesc.isEmpty()) { + tagLine.addText(" - "); + tagLine.addText(tagDesc.trim()); + } + + jsDocLines.add(tagLine); + } + } } private void extractJavadoc(Method method) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java index e3c1dd8bb..a54e64fe3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DTSJSDocParser.java @@ -13,7 +13,7 @@ public class DTSJSDocParser { "/\\*\\*\\s*(.*?)\\*/", Pattern.DOTALL); private static final Pattern TAG_PATTERN = Pattern.compile( - "@(\\w+)(?:\\s+(.*))?"); + "@(\\w+)(?:\\s+\\{([^}]+)\\})?(?:\\s+(.*))?"); private static final Pattern PARAM_PATTERN = Pattern.compile( "@param(?:\\s+\\{([^}]+)\\})?(?:\\s+(\\w+))?(?:\\s+(.*))?"); @@ -136,23 +136,20 @@ private static JSDocTag parseTag(String line, JSDocInfo info) { Matcher tagMatcher = TAG_PATTERN.matcher(line); if (tagMatcher.find()) { String tagName = tagMatcher.group(1); - String rest = tagMatcher.group(2); + String typeName = tagMatcher.group(2); + String rest = tagMatcher.group(3); switch (tagName) { - case "since": - JSDocSinceTag sinceTag = JSDocSinceTag.createSimple(rest != null ? rest.trim() : ""); - info.setSinceTag(sinceTag); - return sinceTag; - case "deprecated": - JSDocDeprecatedTag deprecatedTag = JSDocDeprecatedTag.createSimple(rest != null ? rest.trim() : ""); - info.setDeprecatedTag(deprecatedTag); - return deprecatedTag; case "see": JSDocSeeTag seeTag = JSDocSeeTag.createSimple(rest != null ? rest.trim() : ""); info.addSeeTag(seeTag); return seeTag; default: JSDocTag genericTag = new JSDocTag(tagName, -1, -1, -1); + if (typeName != null) { + typeName = typeName.trim(); + genericTag.setType(typeName, null, -1, -1); + } genericTag.setDescription(rest != null ? rest.trim() : ""); info.addTag(genericTag); return genericTag; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java deleted file mode 100644 index f5f32d503..000000000 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocDeprecatedTag.java +++ /dev/null @@ -1,34 +0,0 @@ -package noppes.npcs.client.gui.util.script.interpreter.jsdoc; - -public class JSDocDeprecatedTag extends JSDocTag { - - private final String reason; - - public JSDocDeprecatedTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String reason) { - super(tagName, atSignOffset, tagNameStart, tagNameEnd); - this.reason = reason; - this.setDescription(reason); - } - - public static JSDocDeprecatedTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, - String reason) { - return new JSDocDeprecatedTag(tagName, atSignOffset, tagNameStart, tagNameEnd, reason); - } - - public static JSDocDeprecatedTag createSimple(String reason) { - return new JSDocDeprecatedTag("deprecated", -1, -1, -1, reason); - } - - public String getReason() { - return reason; - } - - public boolean hasReason() { - return reason != null && !reason.isEmpty(); - } - - @Override - public String toString() { - return "JSDocDeprecatedTag{@deprecated" + (hasReason() ? " " + reason : "") + "}"; - } -} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java index b497da434..899239884 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocInfo.java @@ -19,8 +19,6 @@ public class JSDocInfo { private JSDocTypeTag typeTag; private final List paramTags = new ArrayList<>(); private JSDocReturnTag returnTag; - private JSDocDeprecatedTag deprecatedTag; - private JSDocSinceTag sinceTag; private final List seeTags = new ArrayList<>(); private String description; private final List allTags = new ArrayList<>(); @@ -46,15 +44,6 @@ public void setReturnTag(JSDocReturnTag returnTag) { allTags.add(returnTag); } - public void setDeprecatedTag(JSDocDeprecatedTag deprecatedTag) { - this.deprecatedTag = deprecatedTag; - allTags.add(deprecatedTag); - } - - public void setSinceTag(JSDocSinceTag sinceTag) { - this.sinceTag = sinceTag; - allTags.add(sinceTag); - } public void addSeeTag(JSDocSeeTag seeTag) { this.seeTags.add(seeTag); @@ -102,12 +91,6 @@ public TypeInfo getReturnType() { return returnTag != null ? returnTag.getTypeInfo() : null; } - public JSDocDeprecatedTag getDeprecatedTag() { return deprecatedTag; } - public boolean isDeprecated() { return deprecatedTag != null; } - - public JSDocSinceTag getSinceTag() { return sinceTag; } - public boolean hasSince() { return sinceTag != null; } - public List getSeeTags() { return Collections.unmodifiableList(seeTags); } public boolean hasSeeTags() { return !seeTags.isEmpty(); } @@ -120,8 +103,6 @@ public String toString() { if (typeTag != null) sb.append("type=").append(typeTag.getTypeName()); if (!paramTags.isEmpty()) sb.append(", params=").append(paramTags.size()); if (returnTag != null) sb.append(", return=").append(returnTag.getTypeName()); - if (deprecatedTag != null) sb.append(", deprecated"); - if (sinceTag != null) sb.append(", since=").append(sinceTag.getVersion()); if (!seeTags.isEmpty()) sb.append(", see=").append(seeTags.size()); sb.append("}"); return sb.toString(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java index 2d97d257e..8692965db 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocParser.java @@ -18,16 +18,16 @@ public class JSDocParser { "/\\*\\*([\\s\\S]*?)\\*/", Pattern.MULTILINE); private static final Pattern TYPE_TAG_PATTERN = Pattern.compile( - "@(type)\\s*\\{([^}]+)\\}(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); + "^@(type)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); private static final Pattern PARAM_TAG_PATTERN = Pattern.compile( - "@(param)\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); + "^@param\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); private static final Pattern RETURN_TAG_PATTERN = Pattern.compile( - "@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*([^@\\n]*))?", Pattern.CASE_INSENSITIVE); + "^@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); - private static final Pattern ANY_TAG_PATTERN = Pattern.compile( - "@(\\w+)", Pattern.CASE_INSENSITIVE); + private static final Pattern GENERIC_TAG_PATTERN = Pattern.compile( + "^@(\\w+)(?:\\s*\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE); private final ScriptDocument document; @@ -50,10 +50,53 @@ public JSDocInfo parse(String comment, int commentStart) { int contentOffset = commentStart + 3; - parseTypeTag(content, contentOffset, info); - parseParamTags(content, contentOffset, info); - parseReturnTag(content, contentOffset, info); - extractDescription(content, info); + StringBuilder descriptionBuilder = new StringBuilder(); + boolean foundFirstTag = false; + JSDocTag lastTag = null; + + int position = 0; + while (position <= content.length()) { + int lineEnd = content.indexOf('\n', position); + if (lineEnd == -1) { + lineEnd = content.length(); + } + + String rawLine = content.substring(position, lineEnd); + if (rawLine.endsWith("\r")) { + rawLine = rawLine.substring(0, rawLine.length() - 1); + } + + CleanLine cleanLine = cleanLine(rawLine, contentOffset + position); + String line = cleanLine.text; + + if (!line.isEmpty()) { + if (line.startsWith("@")) { + foundFirstTag = true; + lastTag = parseTagLine(line, cleanLine.offset, info); + } else if (!foundFirstTag) { + if (descriptionBuilder.length() > 0) { + descriptionBuilder.append(" "); + } + descriptionBuilder.append(line); + } else if (lastTag != null) { + String existing = lastTag.getDescription(); + if (existing == null || existing.isEmpty()) { + lastTag.setDescription(line); + } else { + lastTag.setDescription(existing + " " + line); + } + } + } + + if (lineEnd == content.length()) { + break; + } + position = lineEnd + 1; + } + + if (descriptionBuilder.length() > 0) { + info.setDescription(descriptionBuilder.toString().trim()); + } return info; } @@ -97,39 +140,16 @@ public JSDocInfo extractJSDocBefore(String source, int position) { return parse(comment, startCommentPos); } - private void parseTypeTag(String content, int contentOffset, JSDocInfo info) { - Matcher m = TYPE_TAG_PATTERN.matcher(content); - if (m.find()) { - int atSignOffset = contentOffset + m.start(); - int tagNameStart = contentOffset + m.start(1); - int tagNameEnd = contentOffset + m.end(1); - - String typeName = m.group(2).trim(); - String description = m.group(3); - - int braceStart = content.indexOf('{', m.start()); - int braceEnd = content.indexOf('}', braceStart); - int typeStart = contentOffset + braceStart + 1; - int typeEnd = contentOffset + braceEnd; - - TypeInfo typeInfo = resolveType(typeName); - - JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd, - typeName, typeInfo, typeStart, typeEnd, description != null ? description.trim() : null); - info.setTypeTag(tag); - } - } - - private void parseParamTags(String content, int contentOffset, JSDocInfo info) { - Matcher m = PARAM_TAG_PATTERN.matcher(content); - while (m.find()) { - int atSignOffset = contentOffset + m.start(); - int tagNameStart = contentOffset + m.start(1); - int tagNameEnd = contentOffset + m.end(1); + private JSDocTag parseTagLine(String line, int lineOffset, JSDocInfo info) { + Matcher paramMatcher = PARAM_TAG_PATTERN.matcher(line); + if (paramMatcher.matches()) { + String typeName = paramMatcher.group(1); + String paramName = paramMatcher.group(2); + String description = normalizeDescription(paramMatcher.group(3)); - String typeName = m.group(2); - String paramName = m.group(3); - String description = m.group(4); + int atSignOffset = lineOffset + paramMatcher.start(); + int tagNameStart = lineOffset + 1; + int tagNameEnd = tagNameStart + "param".length(); int typeStart = -1; int typeEnd = -1; @@ -137,34 +157,35 @@ private void parseParamTags(String content, int contentOffset, JSDocInfo info) { if (typeName != null) { typeName = typeName.trim(); - int braceStart = content.indexOf('{', m.start()); - int braceEnd = content.indexOf('}', braceStart); - typeStart = contentOffset + braceStart + 1; - typeEnd = contentOffset + braceEnd; + typeStart = lineOffset + paramMatcher.start(1); + typeEnd = lineOffset + paramMatcher.end(1); typeInfo = resolveType(typeName); } - int paramNameStart = contentOffset + m.start(3); - int paramNameEnd = contentOffset + m.end(3); + int paramNameStart = -1; + int paramNameEnd = -1; + if (paramName != null) { + paramNameStart = lineOffset + paramMatcher.start(2); + paramNameEnd = lineOffset + paramMatcher.end(2); + } JSDocParamTag tag = JSDocParamTag.create(atSignOffset, tagNameStart, tagNameEnd, typeName, typeInfo, typeStart, typeEnd, paramName, paramNameStart, paramNameEnd, - description != null ? description.trim() : null); + description); info.addParamTag(tag); + return tag; } - } - private void parseReturnTag(String content, int contentOffset, JSDocInfo info) { - Matcher m = RETURN_TAG_PATTERN.matcher(content); - if (m.find()) { - String tagName = m.group(1); - int atSignOffset = contentOffset + m.start(); - int tagNameStart = contentOffset + m.start(1); - int tagNameEnd = contentOffset + m.end(1); + Matcher returnMatcher = RETURN_TAG_PATTERN.matcher(line); + if (returnMatcher.matches()) { + String tagName = returnMatcher.group(1); + String typeName = returnMatcher.group(2); + String description = normalizeDescription(returnMatcher.group(3)); - String typeName = m.group(2); - String description = m.group(3); + int atSignOffset = lineOffset + returnMatcher.start(); + int tagNameStart = lineOffset + returnMatcher.start(1); + int tagNameEnd = lineOffset + returnMatcher.end(1); int typeStart = -1; int typeEnd = -1; @@ -172,41 +193,127 @@ private void parseReturnTag(String content, int contentOffset, JSDocInfo info) { if (typeName != null) { typeName = typeName.trim(); - int braceStart = content.indexOf('{', m.start()); - int braceEnd = content.indexOf('}', braceStart); - typeStart = contentOffset + braceStart + 1; - typeEnd = contentOffset + braceEnd; + typeStart = lineOffset + returnMatcher.start(2); + typeEnd = lineOffset + returnMatcher.end(2); typeInfo = resolveType(typeName); } JSDocReturnTag tag = JSDocReturnTag.create(tagName, atSignOffset, tagNameStart, tagNameEnd, typeName, typeInfo, typeStart, typeEnd, - description != null ? description.trim() : null); + description); info.setReturnTag(tag); + return tag; + } + + Matcher typeMatcher = TYPE_TAG_PATTERN.matcher(line); + if (typeMatcher.matches()) { + String typeName = typeMatcher.group(2); + String description = normalizeDescription(typeMatcher.group(3)); + + int atSignOffset = lineOffset + typeMatcher.start(); + int tagNameStart = lineOffset + typeMatcher.start(1); + int tagNameEnd = lineOffset + typeMatcher.end(1); + + int typeStart = -1; + int typeEnd = -1; + TypeInfo typeInfo = null; + + if (typeName != null) { + typeName = typeName.trim(); + typeStart = lineOffset + typeMatcher.start(2); + typeEnd = lineOffset + typeMatcher.end(2); + typeInfo = resolveType(typeName); + } + + JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd, + typeName, typeInfo, typeStart, typeEnd, description); + info.setTypeTag(tag); + return tag; + } + + Matcher genericMatcher = GENERIC_TAG_PATTERN.matcher(line); + if (genericMatcher.matches()) { + String tagName = genericMatcher.group(1); + String typeName = genericMatcher.group(2); + String description = normalizeDescription(genericMatcher.group(3)); + + int atSignOffset = lineOffset + genericMatcher.start(); + int tagNameStart = lineOffset + genericMatcher.start(1); + int tagNameEnd = lineOffset + genericMatcher.end(1); + + JSDocTag tag = new JSDocTag(tagName, atSignOffset, tagNameStart, tagNameEnd); + + if (typeName != null) { + typeName = typeName.trim(); + int typeStart = lineOffset + genericMatcher.start(2); + int typeEnd = lineOffset + genericMatcher.end(2); + TypeInfo typeInfo = resolveType(typeName); + tag.setType(typeName, typeInfo, typeStart, typeEnd); + } + + if (description != null && !description.isEmpty()) { + tag.setDescription(description); + } + + info.addTag(tag); + return tag; + } + + return null; + } + + private String normalizeDescription(String description) { + if (description == null) { + return null; } + String trimmed = description.trim(); + return trimmed.isEmpty() ? null : trimmed; } - private void extractDescription(String content, JSDocInfo info) { - Matcher m = ANY_TAG_PATTERN.matcher(content); - String description; + private CleanLine cleanLine(String line, int lineStartOffset) { + int index = 0; + while (index < line.length() && Character.isWhitespace(line.charAt(index))) { + index++; + } - if (m.find()) { - description = content.substring(0, m.start()); - } else { - description = content; + if (index + 2 < line.length() && line.startsWith("/**", index)) { + index += 3; + while (index < line.length() && Character.isWhitespace(line.charAt(index))) { + index++; + } } - description = description.replaceAll("(?m)^\\s*\\*\\s?", "").trim(); + if (index < line.length() && line.charAt(index) == '*') { + index++; + if (index < line.length() && line.charAt(index) == ' ') { + index++; + } + } - if (!description.isEmpty()) { - info.setDescription(description); + String cleaned = line.substring(index); + int end = cleaned.length(); + while (end > 0 && Character.isWhitespace(cleaned.charAt(end - 1))) { + end--; } + cleaned = cleaned.substring(0, end); + + return new CleanLine(cleaned, lineStartOffset + index); } private TypeInfo resolveType(String typeName) { return document.resolveType(typeName); } + private static class CleanLine { + private final String text; + private final int offset; + + private CleanLine(String text, int offset) { + this.text = text; + this.offset = offset; + } + } + public static List findAllJSDocComments(String source) { List positions = new ArrayList<>(); Matcher m = JSDOC_PATTERN.matcher(source); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java deleted file mode 100644 index 6c9005b0b..000000000 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/jsdoc/JSDocSinceTag.java +++ /dev/null @@ -1,30 +0,0 @@ -package noppes.npcs.client.gui.util.script.interpreter.jsdoc; - -public class JSDocSinceTag extends JSDocTag { - - private final String version; - - public JSDocSinceTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String version) { - super(tagName, atSignOffset, tagNameStart, tagNameEnd); - this.version = version; - this.setDescription(version); - } - - public static JSDocSinceTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, - String version) { - return new JSDocSinceTag(tagName, atSignOffset, tagNameStart, tagNameEnd, version); - } - - public static JSDocSinceTag createSimple(String version) { - return new JSDocSinceTag("since", -1, -1, -1, version); - } - - public String getVersion() { - return version; - } - - @Override - public String toString() { - return "JSDocSinceTag{@since " + version + "}"; - } -} From 64f8ca657cdd908d734998d0bc46f5eeecc2079c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 04:54:47 +0200 Subject: [PATCH 285/337] Parsed fully qualified name of API java classes in .d.ts files --- .../interpreter/js_parser/JSTypeInfo.java | 9 +++ .../interpreter/js_parser/JSTypeRegistry.java | 14 ++++ .../js_parser/TypeScriptDefinitionParser.java | 72 +++++++++++++++++-- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java index 8bdab8493..0926ceafe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSTypeInfo.java @@ -18,6 +18,9 @@ public class JSTypeInfo { private final String simpleName; // e.g., "InteractEvent" private final String fullName; // e.g., "IPlayerEvent.InteractEvent" private final String namespace; // e.g., "IPlayerEvent" (parent namespace, null for top-level) + + // Java fully-qualified name (e.g., "noppes.npcs.api.entity.IPlayer") + private String javaFqn; // Type parameters (generics) private final List typeParams = new ArrayList<>(); @@ -53,6 +56,11 @@ public JSTypeInfo setJsDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; return this; } + + public JSTypeInfo setJavaFqn(String javaFqn) { + this.javaFqn = javaFqn; + return this; + } public void addMethod(JSMethodInfo method) { method.setContainingType(this); @@ -88,6 +96,7 @@ public void setResolvedParent(JSTypeInfo parent) { public String getSimpleName() { return simpleName; } public String getFullName() { return fullName; } public String getNamespace() { return namespace; } + public String getJavaFqn() { return javaFqn; } public String getExtendsType() { return extendsType; } public JSTypeInfo getResolvedParent() { return resolvedParent; } public JSDocInfo getJsDocInfo() { return jsDocInfo; } 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 89624c1d4..7a4514a62 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 @@ -23,6 +23,9 @@ public class JSTypeRegistry { // All registered types by full name (e.g., "IPlayerEvent.InteractEvent") private final Map types = new LinkedHashMap<>(); + + // Registered types by Java fully-qualified name (e.g., "noppes.npcs.api.entity.IPlayer") + private final Map typesByJavaFqn = new LinkedHashMap<>(); // Type aliases (simple name -> full type name) private final Map typeAliases = new HashMap<>(); @@ -278,6 +281,17 @@ public void initializeFromVsix(File vsixFile) { */ public void registerType(JSTypeInfo type) { types.put(type.getFullName(), type); + if (type.getJavaFqn() != null && !type.getJavaFqn().isEmpty()) { + typesByJavaFqn.put(type.getJavaFqn(), type); + } + } + + /** + * Get a type by Java fully-qualified name. + */ + public JSTypeInfo getTypeByJavaFqn(String javaFqn) { + if (javaFqn == null || javaFqn.isEmpty()) return null; + return typesByJavaFqn.get(javaFqn); } /** 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 32b832ba9..70f1e2d7b 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 @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTag; import java.io.*; import java.util.*; @@ -130,9 +131,10 @@ public void parseDefinitionFile(String content, String fileName) { parseIndexFile(content); return; } - + // Parse regular interface files - parseInterfaceFile(content, null); + String packageName = derivePackageName(fileName); + parseInterfaceFile(content, null, packageName); } /** @@ -208,7 +210,7 @@ private void parseIndexFile(String content) { /** * Parse interface and class definitions from content. */ - private void parseInterfaceFile(String content, String parentNamespace) { + private void parseInterfaceFile(String content, String parentNamespace, String packageName) { // Find exported interfaces Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(content); while (interfaceMatcher.find()) { @@ -222,6 +224,11 @@ private void parseInterfaceFile(String content, String parentNamespace) { if (jsDoc != null) { typeInfo.setJsDocInfo(jsDoc); } + + String javaFqn = findJavaFqn(jsDoc, packageName, typeInfo.getFullName()); + if (javaFqn != null && !javaFqn.isEmpty()) { + typeInfo.setJavaFqn(javaFqn); + } // Parse type parameters if (typeParamsStr != null && !typeParamsStr.isEmpty()) { @@ -253,7 +260,7 @@ private void parseInterfaceFile(String content, String parentNamespace) { // Parse nested interfaces and type aliases within this interface body String fullNamespace = parentNamespace != null ? parentNamespace + "." + interfaceName : interfaceName; - parseNestedTypes(body, fullNamespace); + parseNestedTypes(body, fullNamespace, packageName); } registry.registerType(typeInfo); @@ -354,7 +361,7 @@ private void parseInterfaceFile(String content, String parentNamespace) { // Namespaces contain exported types, so use parseInterfaceFile String fullNamespace = parentNamespace != null ? parentNamespace + "." + namespaceName : namespaceName; - parseInterfaceFile(body, fullNamespace); + parseInterfaceFile(body, fullNamespace, packageName); // Don't call parseNestedTypes here - namespace members are all exported // and will be caught by parseInterfaceFile's INTERFACE_PATTERN } @@ -393,7 +400,7 @@ private void parseTypeParameters(String typeParamsStr, JSTypeInfo typeInfo) { /** * Parse nested types (interfaces and type aliases) within a parent type or namespace. */ - private void parseNestedTypes(String content, String namespace) { + private void parseNestedTypes(String content, String namespace, String packageName) { // Parse nested interfaces Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content); while (nestedInterfaceMatcher.find()) { @@ -402,6 +409,16 @@ private void parseNestedTypes(String content, String namespace) { String extendsClause = nestedInterfaceMatcher.group(3); JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, namespace); + + JSDocInfo jsDoc = extractJSDocBefore(content, nestedInterfaceMatcher.start()); + if (jsDoc != null) { + typeInfo.setJsDocInfo(jsDoc); + } + + String javaFqn = findJavaFqn(jsDoc, packageName, typeInfo.getFullName()); + if (javaFqn != null && !javaFqn.isEmpty()) { + typeInfo.setJavaFqn(javaFqn); + } // Parse type parameters if (typeParamsStr != null && !typeParamsStr.isEmpty()) { @@ -432,6 +449,49 @@ private void parseNestedTypes(String content, String namespace) { // Parse type aliases within this context parseTypeAliases(content, namespace); } + + private String findJavaFqn(JSDocInfo jsDoc, String packageName, String typeFullName) { + String tagged = extractJavaFqnFromJSDoc(jsDoc); + if (tagged != null && !tagged.isEmpty()) { + return tagged; + } + return buildJavaFqnFromPackage(packageName, typeFullName); + } + + private String extractJavaFqnFromJSDoc(JSDocInfo jsDoc) { + if (jsDoc == null) return null; + for (JSDocTag tag : jsDoc.getAllTags()) { + if (tag == null) continue; + if ("javaFqn".equals(tag.getTagName())) { + if (tag.getDescription() != null && !tag.getDescription().trim().isEmpty()) { + return tag.getDescription().trim(); + } + if (tag.getTypeName() != null && !tag.getTypeName().trim().isEmpty()) { + return tag.getTypeName().trim(); + } + } + } + return null; + } + + private String buildJavaFqnFromPackage(String packageName, String typeFullName) { + if (typeFullName == null || typeFullName.isEmpty()) return null; + if (packageName == null || packageName.isEmpty()) return null; + return packageName + "." + typeFullName; + } + + private String derivePackageName(String fileName) { + if (fileName == null || fileName.isEmpty()) return null; + String normalized = fileName.replace('\\', '/'); + if (normalized.endsWith(".d.ts")) { + normalized = normalized.substring(0, normalized.length() - 5); + } + int lastSlash = normalized.lastIndexOf('/'); + if (lastSlash < 0) return null; + String pkgPath = normalized.substring(0, lastSlash); + if (pkgPath.isEmpty()) return null; + return pkgPath.replace('/', '.'); + } /** * Parse type aliases (export type X = Y). From 3f6972f9f649d8051066d57058d3b14e794c6a44 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 04:56:42 +0200 Subject: [PATCH 286/337] Bridged over JSDocs/param names for fields/methods of java reflected classes that have a matching JS type twin loaded from .d.ts files --- .../interpreter/bridge/DtsJavaBridge.java | 167 ++++++++++++++++++ .../script/interpreter/field/FieldInfo.java | 17 +- .../interpreter/hover/TokenHoverInfo.java | 18 ++ .../script/interpreter/method/MethodInfo.java | 31 +++- 4 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java new file mode 100644 index 000000000..40e97531a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java @@ -0,0 +1,167 @@ +package noppes.npcs.client.gui.util.script.interpreter.bridge; + +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; + +public final class DtsJavaBridge { + + private DtsJavaBridge() {} + + public static JSMethodInfo findMatchingMethod(Method method, TypeInfo containingType) { + JSTypeInfo jsType = findJSTypeInfo(method, containingType); + if (jsType == null) return null; + + List overloads = jsType.getMethodOverloads(method.getName()); + if (overloads.isEmpty()) return null; + + Class[] paramTypes = method.getParameterTypes(); + JSMethodInfo best = null; + int bestScore = -1; + + for (JSMethodInfo candidate : overloads) { + if (candidate.getParameterCount() != paramTypes.length) continue; + int score = scoreOverload(candidate, paramTypes, containingType); + if (score > bestScore) { + bestScore = score; + best = candidate; + } + } + + return bestScore >= 0 ? best : null; + } + + public static JSFieldInfo findMatchingField(Field field, TypeInfo containingType) { + JSTypeInfo jsType = findJSTypeInfo(field, containingType); + if (jsType == null) return null; + return jsType.getField(field.getName()); + } + + private static int scoreOverload(JSMethodInfo method, Class[] paramTypes, TypeInfo containingType) { + int score = 0; + List jsParams = method.getParameters(); + for (int i = 0; i < paramTypes.length; i++) { + int paramScore = scoreParam(paramTypes[i], jsParams.get(i), containingType); + if (paramScore < 0) return -1; + score += paramScore; + } + return score; + } + + private static int scoreParam(Class javaParam, JSMethodInfo.JSParameterInfo jsParam, TypeInfo containingType) { + if (javaParam == null || jsParam == null) return -1; + + TypeInfo resolved = jsParam.getResolvedType(containingType); + if (resolved != null && resolved.getJavaClass() != null) { + Class jsClass = resolved.getJavaClass(); + if (javaParam.equals(jsClass)) return 4; + if (jsClass.isAssignableFrom(javaParam)) return 3; + if (javaParam.isAssignableFrom(jsClass)) return 2; + } + + String jsTypeName = jsParam.getType(); + if (jsTypeName == null || jsTypeName.isEmpty()) return -1; + + if (matchesPrimitive(jsTypeName, javaParam)) { + return 3; + } + + if (javaParam.isArray() && jsTypeName.endsWith("[]")) { + String elementType = jsTypeName.substring(0, jsTypeName.length() - 2); + return matchesArrayElement(javaParam.getComponentType(), elementType, containingType) ? 2 : -1; + } + + JSTypeInfo jsType = resolveJSTypeInfo(jsTypeName); + if (jsType != null && jsType.getJavaFqn() != null) { + String javaFqn = normalizeJavaFqn(jsType.getJavaFqn()); + String paramFqn = normalizeJavaFqn(javaParam.getName()); + if (javaFqn.equals(paramFqn)) return 3; + } + + if (jsTypeName.equals(javaParam.getSimpleName())) return 1; + if (jsTypeName.equals(javaParam.getName())) return 1; + if ("any".equals(jsTypeName)) return 1; + return -1; + } + + private static boolean matchesArrayElement(Class elementClass, String jsElementType, TypeInfo containingType) { + if (elementClass == null) return false; + if (matchesPrimitive(jsElementType, elementClass)) return true; + + JSTypeInfo jsType = resolveJSTypeInfo(jsElementType); + if (jsType != null && jsType.getJavaFqn() != null) { + String javaFqn = normalizeJavaFqn(jsType.getJavaFqn()); + String elementFqn = normalizeJavaFqn(elementClass.getName()); + return javaFqn.equals(elementFqn); + } + + return jsElementType.equals(elementClass.getSimpleName()) || jsElementType.equals(elementClass.getName()); + } + + private static boolean matchesPrimitive(String jsTypeName, Class javaParam) { + switch (jsTypeName) { + case "string": + return javaParam == String.class || javaParam == char.class || javaParam == Character.class; + case "boolean": + return javaParam == boolean.class || javaParam == Boolean.class; + case "number": + return isNumberType(javaParam); + case "void": + return javaParam == void.class || javaParam == Void.class; + default: + return false; + } + } + + private static boolean isNumberType(Class type) { + if (type == null) return false; + return type == byte.class || type == short.class || type == int.class || type == long.class + || type == float.class || type == double.class + || Number.class.isAssignableFrom(type); + } + + private static JSTypeInfo findJSTypeInfo(Method method, TypeInfo containingType) { + if (containingType != null && containingType.isJSType()) { + return containingType.getJSTypeInfo(); + } + + Class javaClass = containingType != null ? containingType.getJavaClass() : method.getDeclaringClass(); + return resolveJSTypeInfo(javaClass != null ? javaClass.getName() : null); + } + + private static JSTypeInfo findJSTypeInfo(Field field, TypeInfo containingType) { + if (containingType != null && containingType.isJSType()) { + return containingType.getJSTypeInfo(); + } + + Class javaClass = containingType != null ? containingType.getJavaClass() : field.getDeclaringClass(); + return resolveJSTypeInfo(javaClass != null ? javaClass.getName() : null); + } + + private static JSTypeInfo resolveJSTypeInfo(String javaFqnOrType) { + if (javaFqnOrType == null || javaFqnOrType.isEmpty()) return null; + JSTypeRegistry registry = TypeResolver.getInstance().getJSTypeRegistry(); + JSTypeInfo direct = registry.getTypeByJavaFqn(javaFqnOrType); + if (direct != null) return direct; + + String normalized = normalizeJavaFqn(javaFqnOrType); + if (!normalized.equals(javaFqnOrType)) { + JSTypeInfo normalizedType = registry.getTypeByJavaFqn(normalized); + if (normalizedType != null) return normalizedType; + } + + return registry.getType(javaFqnOrType); + } + + private static String normalizeJavaFqn(String javaFqn) { + if (javaFqn == null) return null; + return javaFqn.replace('$', '.'); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 55834d311..bbf74f644 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -3,6 +3,7 @@ import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; +import noppes.npcs.client.gui.util.script.interpreter.bridge.DtsJavaBridge; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -158,7 +159,21 @@ public static FieldInfo fromReflection(Field field, TypeInfo containingType) { return constantInfo.getFieldInfo(); } - return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, null, -1, -1, field.getModifiers(), field); + // Try to find matching JSFieldInfo to bridge over documentation + JSFieldInfo jsField = DtsJavaBridge.findMatchingField(field, containingType); + String documentation = null; + JSDocInfo jsDocInfo = null; + if (jsField != null) { + jsDocInfo = jsField.getJsDocInfo(); + String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null; + documentation = jsDocDesc != null ? jsDocDesc : jsField.getDocumentation(); + } + + FieldInfo fieldInfo = new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, field.getModifiers(), field); + if (jsDocInfo != null) { + fieldInfo.setJSDocInfo(jsDocInfo); + } + return fieldInfo; } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 40963b165..381b817d0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -736,6 +736,10 @@ private void extractMethodCallInfo(Token token) { } // Try to get actual Java method for more details + if (methodInfo != null && shouldPreferMethodInfo(methodInfo)) { + buildBasicMethodDeclaration(methodInfo, containingType); + return; + } if (methodInfo != null && methodInfo.getJavaMethod() != null) { buildMethodDeclaration(methodInfo.getJavaMethod(), containingType); extractJavadoc(methodInfo.getJavaMethod()); @@ -1110,6 +1114,20 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain } + private boolean shouldPreferMethodInfo(MethodInfo methodInfo) { + if (methodInfo.getJSDocInfo() != null) return true; + String doc = methodInfo.getDocumentation(); + if (doc != null && !doc.isEmpty()) return true; + + for (FieldInfo param : methodInfo.getParameters()) { + String name = param.getName(); + if (name != null && !name.matches("arg\\d+")) { + return true; + } + } + return false; + } + /** * Format JSDoc information in IntelliJ-style with "Params:" and "Returns:" sections. * Creates colored documentation lines with parameter names highlighted. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 859d3c0e9..60987e8cc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -2,6 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.*; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.bridge.DtsJavaBridge; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; @@ -144,15 +145,37 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) String name = method.getName(); TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); int modifiers = method.getModifiers(); - + + // Try to find matching JSFieldInfo to bridge parameter names and docs + JSMethodInfo jsMethod = DtsJavaBridge.findMatchingMethod(method, containingType); List params = new ArrayList<>(); Class[] paramTypes = method.getParameterTypes(); + List jsParams = jsMethod != null ? jsMethod.getParameters() : null; for (int i = 0; i < paramTypes.length; i++) { TypeInfo paramType = TypeInfo.fromClass(paramTypes[i]); - params.add(FieldInfo.reflectionParam("arg" + i, paramType)); + String paramName = "arg" + i; + if (jsParams != null && i < jsParams.size()) { + String jsName = jsParams.get(i).getName(); + if (jsName != null && !jsName.isEmpty()) { + paramName = jsName; + } + } + params.add(FieldInfo.reflectionParam(paramName, paramType)); } - - return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, null, method); + + String documentation = null; + JSDocInfo jsDocInfo = null; + if (jsMethod != null) { + jsDocInfo = jsMethod.getJsDocInfo(); + String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null; + documentation = jsDocDesc != null ? jsDocDesc : jsMethod.getDocumentation(); + } + + MethodInfo methodInfo = new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, method); + if (jsDocInfo != null) { + methodInfo.setJSDocInfo(jsDocInfo); + } + return methodInfo; } /** From 52db241b9a53042dab30588551df08412268cd03 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 05:36:16 +0200 Subject: [PATCH 287/337] DtsJavaBridge interface/superclass fallback Cached DtsJavaBridge lookups Java varargs now match DTS reset params (...x: T) --- .../interpreter/bridge/DtsJavaBridge.java | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java index 40e97531a..eb8d67a87 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java @@ -10,12 +10,20 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; public final class DtsJavaBridge { + private static final Map METHOD_CACHE = new ConcurrentHashMap<>(); + private static final Map FIELD_CACHE = new ConcurrentHashMap<>(); + private DtsJavaBridge() {} public static JSMethodInfo findMatchingMethod(Method method, TypeInfo containingType) { + JSMethodInfo cached = METHOD_CACHE.get(method); + if (cached != null) return cached; + JSTypeInfo jsType = findJSTypeInfo(method, containingType); if (jsType == null) return null; @@ -28,34 +36,45 @@ public static JSMethodInfo findMatchingMethod(Method method, TypeInfo containing for (JSMethodInfo candidate : overloads) { if (candidate.getParameterCount() != paramTypes.length) continue; - int score = scoreOverload(candidate, paramTypes, containingType); + int score = scoreOverload(candidate, paramTypes, containingType, method.isVarArgs()); if (score > bestScore) { bestScore = score; best = candidate; } } - return bestScore >= 0 ? best : null; + if (bestScore >= 0 && best != null) { + METHOD_CACHE.put(method, best); + return best; + } + return null; } public static JSFieldInfo findMatchingField(Field field, TypeInfo containingType) { + JSFieldInfo cached = FIELD_CACHE.get(field); + if (cached != null) return cached; + JSTypeInfo jsType = findJSTypeInfo(field, containingType); if (jsType == null) return null; - return jsType.getField(field.getName()); + JSFieldInfo match = jsType.getField(field.getName()); + if (match != null) { + FIELD_CACHE.put(field, match); + } + return match; } - private static int scoreOverload(JSMethodInfo method, Class[] paramTypes, TypeInfo containingType) { + private static int scoreOverload(JSMethodInfo method, Class[] paramTypes, TypeInfo containingType, boolean isVarArgs) { int score = 0; List jsParams = method.getParameters(); for (int i = 0; i < paramTypes.length; i++) { - int paramScore = scoreParam(paramTypes[i], jsParams.get(i), containingType); + int paramScore = scoreParam(paramTypes[i], jsParams.get(i), containingType, isVarArgs, i == paramTypes.length - 1); if (paramScore < 0) return -1; score += paramScore; } return score; } - private static int scoreParam(Class javaParam, JSMethodInfo.JSParameterInfo jsParam, TypeInfo containingType) { + private static int scoreParam(Class javaParam, JSMethodInfo.JSParameterInfo jsParam, TypeInfo containingType, boolean isVarArgs, boolean isLastParam) { if (javaParam == null || jsParam == null) return -1; TypeInfo resolved = jsParam.getResolvedType(containingType); @@ -69,6 +88,12 @@ private static int scoreParam(Class javaParam, JSMethodInfo.JSParameterInfo j String jsTypeName = jsParam.getType(); if (jsTypeName == null || jsTypeName.isEmpty()) return -1; + if (isVarArgs && isLastParam && javaParam.isArray()) { + if (matchesArrayElement(javaParam.getComponentType(), jsTypeName, containingType)) { + return 2; + } + } + if (matchesPrimitive(jsTypeName, javaParam)) { return 3; } @@ -133,7 +158,7 @@ private static JSTypeInfo findJSTypeInfo(Method method, TypeInfo containingType) } Class javaClass = containingType != null ? containingType.getJavaClass() : method.getDeclaringClass(); - return resolveJSTypeInfo(javaClass != null ? javaClass.getName() : null); + return resolveJSTypeInfo(javaClass); } private static JSTypeInfo findJSTypeInfo(Field field, TypeInfo containingType) { @@ -142,7 +167,26 @@ private static JSTypeInfo findJSTypeInfo(Field field, TypeInfo containingType) { } Class javaClass = containingType != null ? containingType.getJavaClass() : field.getDeclaringClass(); - return resolveJSTypeInfo(javaClass != null ? javaClass.getName() : null); + return resolveJSTypeInfo(javaClass); + } + + private static JSTypeInfo resolveJSTypeInfo(Class javaClass) { + if (javaClass == null) return null; + + JSTypeInfo direct = resolveJSTypeInfo(javaClass.getName()); + if (direct != null) return direct; + + for (Class iface : javaClass.getInterfaces()) { + JSTypeInfo ifaceType = resolveJSTypeInfo(iface.getName()); + if (ifaceType != null) return ifaceType; + } + + Class superClass = javaClass.getSuperclass(); + if (superClass != null && superClass != Object.class) { + return resolveJSTypeInfo(superClass); + } + + return null; } private static JSTypeInfo resolveJSTypeInfo(String javaFqnOrType) { From 26bf90ac66c71ebe3cefee395484742b9ac0ea6f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 05:36:59 +0200 Subject: [PATCH 288/337] Improved TokenHoverRenderer box height calculations --- .../interpreter/hover/TokenHoverRenderer.java | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java index 4af1352cb..7f6e34051 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverRenderer.java @@ -145,8 +145,8 @@ private static int getMaxContentWidth(int viewportX, int viewportWidth, int toke */ private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, TokenHoverInfo info) { GL11.glDisable(GL11.GL_SCISSOR_TEST); - - int boxHeight = calculateContentHeight(info, wrapWidth) + PADDING * 1; + + int boxHeight = calculateContentHeight(info, wrapWidth) + PADDING * 2; // Draw background Gui.drawRect(x, y, x + boxWidth, y + boxHeight, BG_COLOR); @@ -468,18 +468,16 @@ private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) // JSDoc-formatted documentation lines if (jsDocLines != null && !jsDocLines.isEmpty()) { - // Add separator if there was plain documentation or declaration but no plain docs if (docs.isEmpty() && !declaration.isEmpty()) { - // totalHeight += SEPARATOR_SPACING + SEPARATOR_HEIGHT; + totalHeight += SEPARATOR_HEIGHT + SEPARATOR_SPACING; } - + for (TokenHoverInfo.DocumentationLine docLine : jsDocLines) { if (!docLine.isEmpty()) { totalHeight += calculateSegmentsHeight(contentWidth, docLine.segments); - //. totalHeight += LINE_SPACING; + totalHeight += LINE_SPACING; } else { - // Empty line - // totalHeight += lineHeight + LINE_SPACING; + totalHeight += lineHeight + LINE_SPACING; } } } @@ -520,7 +518,12 @@ private static int calculateSegmentsHeight(int maxWidth, List Date: Sat, 24 Jan 2026 06:42:38 +0200 Subject: [PATCH 289/337] Loaded all .d.ts files in assets/modId/api directory of ALL loaded mods --- .../interpreter/js_parser/DtsModScanner.java | 197 ++++++++++++++++++ .../interpreter/js_parser/JSTypeRegistry.java | 125 ++++++++--- 2 files changed, 298 insertions(+), 24 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java new file mode 100644 index 000000000..f8aa926c0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/DtsModScanner.java @@ -0,0 +1,197 @@ +package noppes.npcs.client.gui.util.script.interpreter.js_parser; + +import cpw.mods.fml.common.Loader; +import cpw.mods.fml.common.ModContainer; + +import java.io.*; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class DtsModScanner { + + private static final Pattern MOD_DTS_PATH_PATTERN = Pattern.compile("^assets/([^/]+)/api/(.+\\.d\\.ts)$"); + + private static final List DOMAIN_PRIORITY = Arrays.asList("customnpcs", "npcdbc"); + + private DtsModScanner() {} + + static List collectDtsFilesFromMods() { + List dtsFiles = new ArrayList<>(); + for (ModContainer mod : Loader.instance().getModList()) { + File source = mod.getSource(); + if (source == null || !source.exists()) { + continue; + } + if (source.isDirectory()) { + scanDirectoryForModDts(source, mod.getModId(), dtsFiles); + } else if (source.getName().endsWith(".jar") || source.getName().endsWith(".zip")) { + scanJarForModDts(source, mod.getModId(), dtsFiles); + } + } + return dtsFiles; + } + + static void sortDtsFiles(List dtsFiles) { + Collections.sort(dtsFiles, new DtsFileRefComparator()); + } + + static void logSummary(List dtsFiles) { + Map domainCounts = new LinkedHashMap<>(); + for (DtsFileRef ref : dtsFiles) { + domainCounts.put(ref.getDomain(), domainCounts.getOrDefault(ref.getDomain(), 0) + 1); + } + System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files across " + domainCounts.size() + " domains"); + for (Map.Entry entry : domainCounts.entrySet()) { + System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files"); + } + } + + private static void scanDirectoryForModDts(File baseDir, String modId, List dtsFiles) { + scanDirectoryForModDts(baseDir, baseDir, modId, dtsFiles); + } + + private static void scanDirectoryForModDts(File baseDir, File directory, String modId, List dtsFiles) { + File[] files = directory.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + scanDirectoryForModDts(baseDir, file, modId, dtsFiles); + continue; + } + if (!file.getName().endsWith(".d.ts")) { + continue; + } + String relativePath = baseDir.toURI().relativize(file.toURI()).getPath(); + if (relativePath == null) { + continue; + } + String normalized = relativePath.replace('\\', '/'); + Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(normalized); + if (matcher.matches()) { + String domain = matcher.group(1); + String apiPath = matcher.group(2); + dtsFiles.add(DtsFileRef.forFile(modId, domain, apiPath, file)); + } + } + } + + private static void scanJarForModDts(File jarFile, String modId, List dtsFiles) { + try { + JarFile jar = new JarFile(jarFile); + Enumeration entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(entryName); + if (matcher.matches()) { + String domain = matcher.group(1); + String apiPath = matcher.group(2); + dtsFiles.add(DtsFileRef.forJar(modId, domain, apiPath, jarFile, entryName)); + } + } + jar.close(); + } catch (Exception e) { + System.err.println("[JSTypeRegistry] Error scanning mod jar for .d.ts files: " + e.getMessage()); + } + } + + static final class DtsFileRef { + private final String modId; + private final String domain; + private final String relativePath; + private final File file; + private final File jarFile; + private final String jarEntryName; + + private DtsFileRef(String modId, String domain, String relativePath, File file, File jarFile, String jarEntryName) { + this.modId = modId; + this.domain = domain; + this.relativePath = relativePath; + this.file = file; + this.jarFile = jarFile; + this.jarEntryName = jarEntryName; + } + + static DtsFileRef forFile(String modId, String domain, String relativePath, File file) { + return new DtsFileRef(modId, domain, relativePath, file, null, null); + } + + static DtsFileRef forJar(String modId, String domain, String relativePath, File jarFile, String jarEntryName) { + return new DtsFileRef(modId, domain, relativePath, null, jarFile, jarEntryName); + } + + InputStream openStream() throws IOException { + if (file != null) { + return new FileInputStream(file); + } + if (jarFile != null && jarEntryName != null) { + final JarFile jar = new JarFile(jarFile); + JarEntry entry = jar.getJarEntry(jarEntryName); + if (entry == null) { + jar.close(); + return null; + } + InputStream is = jar.getInputStream(entry); + return new FilterInputStream(is) { + @Override + public void close() throws IOException { + super.close(); + jar.close(); + } + }; + } + return null; + } + + String getDomain() { + return domain; + } + + String getRelativePath() { + return relativePath; + } + + String getOrigin() { + return modId + ":" + domain + ":" + relativePath; + } + } + + private static class DtsFileRefComparator implements Comparator { + @Override + public int compare(DtsFileRef a, DtsFileRef b) { + int domainCompare = Integer.compare(getDomainRank(a.getDomain()), getDomainRank(b.getDomain())); + if (domainCompare != 0) { + return domainCompare; + } + int domainNameCompare = a.getDomain().compareTo(b.getDomain()); + if (domainNameCompare != 0) { + return domainNameCompare; + } + int filePriorityCompare = Integer.compare(getFilePriority(a.getRelativePath()), getFilePriority(b.getRelativePath())); + if (filePriorityCompare != 0) { + return filePriorityCompare; + } + return a.getRelativePath().compareTo(b.getRelativePath()); + } + + private int getDomainRank(String domain) { + int idx = DOMAIN_PRIORITY.indexOf(domain); + return idx >= 0 ? idx : DOMAIN_PRIORITY.size(); + } + + private int getFilePriority(String relativePath) { + if (relativePath.endsWith("hooks.d.ts")) { + return 0; + } + if (relativePath.endsWith("index.d.ts")) { + return 1; + } + return 2; + } + } +} 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 7a4514a62..de7e92aa2 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 @@ -6,6 +6,7 @@ import java.io.*; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -18,9 +19,6 @@ public class JSTypeRegistry { private static JSTypeRegistry INSTANCE; - // Resource location for the API definitions - private static final String API_RESOURCE_PATH = "customnpcs:api/"; - // All registered types by full name (e.g., "IPlayerEvent.InteractEvent") private final Map types = new LinkedHashMap<>(); @@ -29,6 +27,12 @@ public class JSTypeRegistry { // Type aliases (simple name -> full type name) private final Map typeAliases = new HashMap<>(); + + // Type full name -> origin string (modId:domain:relativePath) + private final Map typeOrigins = new HashMap<>(); + + // Alias name -> origin string (modId:domain:relativePath) + private final Map aliasOrigins = new HashMap<>(); // Context-aware hook function signatures: namespace -> functionName -> list of signatures // The namespace is the event interface name (e.g., "INpcEvent", "IPlayerEvent") @@ -53,6 +57,8 @@ public class JSTypeRegistry { private boolean initialized = false; private boolean initializationAttempted = false; + + private String currentSource = null; public static JSTypeRegistry getInstance() { if (INSTANCE == null) { @@ -73,25 +79,28 @@ public void initializeFromResources() { try { TypeScriptDefinitionParser parser = new TypeScriptDefinitionParser(this); - - // Recursively load all .d.ts files from the api directory - Set dtsFiles = findAllDtsFilesInResources("assets/customnpcs/api"); - - System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files in resources"); - - // Load hooks.d.ts and index.d.ts first if they exist (defines core types) - if (dtsFiles.contains("hooks.d.ts")) { - loadResourceFile(parser, "hooks.d.ts"); - } - if (dtsFiles.contains("index.d.ts")) { - loadResourceFile(parser, "index.d.ts"); - } - - // Load all other .d.ts files - for (String filePath : dtsFiles) { - if (!filePath.equals("hooks.d.ts") && !filePath.equals("index.d.ts")) { - loadResourceFile(parser, filePath); + + List dtsFiles = DtsModScanner.collectDtsFilesFromMods(); + if (dtsFiles.isEmpty()) { + Set fallbackFiles = findAllDtsFilesInResources("assets/customnpcs/api"); + + System.out.println("[JSTypeRegistry] Found " + fallbackFiles.size() + " .d.ts files in resources"); + + if (fallbackFiles.contains("hooks.d.ts")) { + loadResourceFile(parser, "hooks.d.ts"); + } + if (fallbackFiles.contains("index.d.ts")) { + loadResourceFile(parser, "index.d.ts"); + } + + for (String filePath : fallbackFiles) { + if (!filePath.equals("hooks.d.ts") && !filePath.equals("index.d.ts")) { + loadResourceFile(parser, filePath); + } } + } else { + DtsModScanner.logSummary(dtsFiles); + loadDtsFiles(parser, dtsFiles); } // Phase 2: Resolve all type parameters now that all types are loaded @@ -113,6 +122,33 @@ public void initializeFromResources() { e.printStackTrace(); } } + + private void loadDtsFiles(TypeScriptDefinitionParser parser, List dtsFiles) { + DtsModScanner.sortDtsFiles(dtsFiles); + for (DtsModScanner.DtsFileRef ref : dtsFiles) { + try (InputStream is = ref.openStream()) { + if (is == null) { + continue; + } + String content = readFully(new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))); + setCurrentSource(ref.getOrigin()); + parser.parseDefinitionFile(content, ref.getRelativePath()); + } catch (Exception e) { + System.err.println("[JSTypeRegistry] Failed to load " + ref.getOrigin() + ": " + e.getMessage()); + } finally { + setCurrentSource(null); + } + } + } + + private String readFully(BufferedReader reader) throws IOException { + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + return sb.toString(); + } /** * Recursively find all .d.ts files in the resources directory. @@ -210,7 +246,7 @@ private void scanJarForDts(String jarPath, String resourcePath, Set dtsF System.err.println("[JSTypeRegistry] Error scanning JAR for .d.ts files: " + e.getMessage()); } } - + /** * Load a specific .d.ts file from resources. */ @@ -280,9 +316,20 @@ public void initializeFromVsix(File vsixFile) { * Register a type. */ public void registerType(JSTypeInfo type) { - types.put(type.getFullName(), type); + String fullName = type.getFullName(); + if (types.containsKey(fullName)) { + logTypeCollision(fullName, getCurrentSource()); + return; + } + types.put(fullName, type); + typeOrigins.put(fullName, getCurrentSource()); if (type.getJavaFqn() != null && !type.getJavaFqn().isEmpty()) { - typesByJavaFqn.put(type.getJavaFqn(), type); + String javaFqn = type.getJavaFqn(); + if (!typesByJavaFqn.containsKey(javaFqn)) { + typesByJavaFqn.put(javaFqn, type); + } else { + System.out.println("[JSTypeRegistry] Duplicate javaFqn " + javaFqn + " from " + getCurrentSource()); + } } } @@ -298,7 +345,15 @@ public JSTypeInfo getTypeByJavaFqn(String javaFqn) { * Register a type alias. */ public void registerTypeAlias(String alias, String fullType) { + if (typeAliases.containsKey(alias)) { + String existing = typeAliases.get(alias); + if (!Objects.equals(existing, fullType)) { + logAliasCollision(alias, existing, fullType, getCurrentSource()); + } + return; + } typeAliases.put(alias, fullType); + aliasOrigins.put(alias, getCurrentSource()); } /** @@ -755,13 +810,35 @@ public boolean isInitialized() { */ public void clear() { types.clear(); + typesByJavaFqn.clear(); typeAliases.clear(); + typeOrigins.clear(); + aliasOrigins.clear(); hooks.clear(); contextHooks.clear(); globalEngineObjects.clear(); initialized = false; } + private void setCurrentSource(String source) { + currentSource = source; + } + + private String getCurrentSource() { + return currentSource == null ? "unknown" : currentSource; + } + + private void logTypeCollision(String fullName, String incomingSource) { + String existingSource = typeOrigins.getOrDefault(fullName, "unknown"); + System.out.println("[JSTypeRegistry] Duplicate type " + fullName + " from " + incomingSource + " (kept " + existingSource + ")"); + } + + private void logAliasCollision(String alias, String existingType, String incomingType, String incomingSource) { + String existingSource = aliasOrigins.getOrDefault(alias, "unknown"); + System.out.println("[JSTypeRegistry] Duplicate alias " + alias + " from " + incomingSource + " (kept " + existingSource + ")"); + System.out.println("[JSTypeRegistry] Alias " + alias + " existing=" + existingType + " incoming=" + incomingType); + } + /** * Represents a hook function signature with its namespace. */ From f1a968d1962b0a8769fa83b116fc710aa9956e58 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 24 Jan 2026 06:47:44 +0200 Subject: [PATCH 290/337] Got bridges working for new JSTypeRegistry domains (.d.ts files of each loaded mod) --- .../gui/util/script/interpreter/bridge/DtsJavaBridge.java | 8 ++++++++ .../util/script/interpreter/js_parser/JSTypeRegistry.java | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java index eb8d67a87..dea2fb882 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java @@ -20,6 +20,11 @@ public final class DtsJavaBridge { private DtsJavaBridge() {} + public static void clearCache() { + METHOD_CACHE.clear(); + FIELD_CACHE.clear(); + } + public static JSMethodInfo findMatchingMethod(Method method, TypeInfo containingType) { JSMethodInfo cached = METHOD_CACHE.get(method); if (cached != null) return cached; @@ -191,6 +196,9 @@ private static JSTypeInfo resolveJSTypeInfo(Class javaClass) { private static JSTypeInfo resolveJSTypeInfo(String javaFqnOrType) { if (javaFqnOrType == null || javaFqnOrType.isEmpty()) return null; + if (javaFqnOrType.startsWith("Java.")) { + javaFqnOrType = javaFqnOrType.substring(5); + } JSTypeRegistry registry = TypeResolver.getInstance().getJSTypeRegistry(); JSTypeInfo direct = registry.getTypeByJavaFqn(javaFqnOrType); if (direct != null) return direct; 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 de7e92aa2..1107aca07 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 @@ -3,6 +3,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.util.ResourceLocation; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.client.gui.util.script.interpreter.bridge.DtsJavaBridge; import java.io.*; import java.net.URL; @@ -817,6 +818,7 @@ public void clear() { hooks.clear(); contextHooks.clear(); globalEngineObjects.clear(); + DtsJavaBridge.clearCache(); initialized = false; } From f43b9ae1865aaf73b9522eb89b6c8223a4dc5a2b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 25 Jan 2026 05:37:30 +0200 Subject: [PATCH 291/337] Allowed for .d.ts patches to be applied and merge into existing types i.e IPlayer.d.ts patch for getDBCPlayer() by npcdbc addon to return IDBCAddon instead of IDBCPlayer --- .../interpreter/js_parser/JSTypeRegistry.java | 112 +++++++++++++++++- 1 file changed, 108 insertions(+), 4 deletions(-) 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 1107aca07..ab6b8f45b 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 @@ -315,17 +315,30 @@ public void initializeFromVsix(File vsixFile) { /** * Register a type. + * If a type with matching javaFqn or fullName already exists, merges instead of discarding. */ public void registerType(JSTypeInfo type) { String fullName = type.getFullName(); - if (types.containsKey(fullName)) { - logTypeCollision(fullName, getCurrentSource()); + String javaFqn = type.getJavaFqn(); + + JSTypeInfo existingType = null; + + if (javaFqn != null && !javaFqn.isEmpty()) { + existingType = typesByJavaFqn.get(javaFqn); + } + + if (existingType == null && types.containsKey(fullName)) { + existingType = types.get(fullName); + } + + if (existingType != null) { + mergeType(existingType, type, getCurrentSource()); return; } + types.put(fullName, type); typeOrigins.put(fullName, getCurrentSource()); - if (type.getJavaFqn() != null && !type.getJavaFqn().isEmpty()) { - String javaFqn = type.getJavaFqn(); + if (javaFqn != null && !javaFqn.isEmpty()) { if (!typesByJavaFqn.containsKey(javaFqn)) { typesByJavaFqn.put(javaFqn, type); } else { @@ -841,6 +854,97 @@ private void logAliasCollision(String alias, String existingType, String incomin System.out.println("[JSTypeRegistry] Alias " + alias + " existing=" + existingType + " incoming=" + incomingType); } + /** + * Find the method key in a type's methods map that matches the given method name and parameter count. + * This handles overload keys (methodName, methodName$1, methodName$2, etc.) + * + * @param type The type to search in + * @param methodName The method name to search for + * @param paramCount The parameter count to match + * @return The method key if found, or null if no match + */ + private String findOwnMethodKeyByParamCount(JSTypeInfo type, String methodName, int paramCount) { + Map methods = type.getMethods(); + + // Check direct key first + JSMethodInfo direct = methods.get(methodName); + if (direct != null && direct.getParameterCount() == paramCount) { + return methodName; + } + + // Check overload keys: methodName$1, methodName$2, etc. + int index = 1; + String overloadKey = methodName + "$" + index; + while (methods.containsKey(overloadKey)) { + JSMethodInfo overload = methods.get(overloadKey); + if (overload.getParameterCount() == paramCount) { + return overloadKey; + } + index++; + overloadKey = methodName + "$" + index; + } + + return null; // No match found + } + + /** + * Merge an incoming type into an existing type. + * This allows addon mods to patch base mod types via .d.ts files. + * + * @param existing The existing type to merge into + * @param incoming The incoming type to merge from + * @param incomingSource The source of the incoming type (for logging) + */ + private void mergeType(JSTypeInfo existing, JSTypeInfo incoming, String incomingSource) { + int methodsMerged = 0; + int fieldsMerged = 0; + + // Merge methods (replace matching overloads by param count) + for (Map.Entry entry : incoming.getMethods().entrySet()) { + JSMethodInfo incomingMethod = entry.getValue(); + String methodName = incomingMethod.getName(); + int paramCount = incomingMethod.getParameterCount(); + + // Find existing method with same param count + String existingKey = findOwnMethodKeyByParamCount(existing, methodName, paramCount); + if (existingKey != null) { + // Replace existing overload + existing.getMethods().put(existingKey, incomingMethod); + incomingMethod.setContainingType(existing); + methodsMerged++; + } else { + // Add as new overload + existing.addMethod(incomingMethod); + methodsMerged++; + } + } + + // Merge fields (replace by name) + for (Map.Entry entry : incoming.getFields().entrySet()) { + String fieldName = entry.getKey(); + JSFieldInfo incomingField = entry.getValue(); + existing.getFields().put(fieldName, incomingField); + incomingField.setContainingType(existing); + fieldsMerged++; + } + + // Merge metadata (fill gaps only - preserve existing non-null values) + if (existing.getJavaFqn() == null && incoming.getJavaFqn() != null) { + existing.setJavaFqn(incoming.getJavaFqn()); + } + if (existing.getJsDocInfo() == null && incoming.getJsDocInfo() != null) { + existing.setJsDocInfo(incoming.getJsDocInfo()); + } + if (existing.getExtendsType() == null && incoming.getExtendsType() != null) { + existing.setExtends(incoming.getExtendsType()); + } + + // Log merge details + System.out.println("[JSTypeRegistry] Merged type " + existing.getFullName() + + " with " + methodsMerged + " methods, " + fieldsMerged + + " fields from " + incomingSource); + } + /** * Represents a hook function signature with its namespace. */ From 46b62eea567ddab212b3595f9d910fd0db5d52d2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 25 Jan 2026 06:22:11 +0200 Subject: [PATCH 292/337] Bridged .d.ts patch overrides to java side to show in autocomplete/hoverinfo --- .../interpreter/bridge/DtsJavaBridge.java | 79 +++++++++++++++++++ .../interpreter/hover/TokenHoverInfo.java | 8 ++ .../script/interpreter/method/MethodInfo.java | 20 ++++- 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java index dea2fb882..cebd1bcfc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/bridge/DtsJavaBridge.java @@ -11,18 +11,97 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public final class DtsJavaBridge { + /** Cache of reflection Method -> matching JS method info (for docs/param names). */ private static final Map METHOD_CACHE = new ConcurrentHashMap<>(); + /** Cache of reflection Field -> matching JS field info (for docs/typing). */ private static final Map FIELD_CACHE = new ConcurrentHashMap<>(); + /** Cache of reflection Method -> resolved Java TypeInfo override from .d.ts. */ + private static final Map RETURN_OVERRIDE_CACHE = new ConcurrentHashMap<>(); + /** Negative cache for methods with no usable override. */ + private static final Set RETURN_OVERRIDE_MISSES = ConcurrentHashMap.newKeySet(); private DtsJavaBridge() {} public static void clearCache() { METHOD_CACHE.clear(); FIELD_CACHE.clear(); + RETURN_OVERRIDE_CACHE.clear(); + RETURN_OVERRIDE_MISSES.clear(); + } + + /** + * Resolve a Java return type override for a reflected method using its .d.ts twin. + * + * The override is used only for editor typing (e.g., Janino hover/autocomplete) and + * never changes runtime reflection. This returns a Java-resolved TypeInfo when the + * .d.ts return type can be mapped to a concrete Java class. + */ + public static TypeInfo resolveReturnTypeOverride(Method method, TypeInfo containingType, JSMethodInfo jsMethod) { + if (method == null || jsMethod == null) return null; + + TypeInfo cached = RETURN_OVERRIDE_CACHE.get(method); + if (cached != null) return cached; + if (RETURN_OVERRIDE_MISSES.contains(method)) return null; + + String jsReturnType = jsMethod.getReturnType(); + if (jsReturnType == null || jsReturnType.isEmpty()) { + RETURN_OVERRIDE_MISSES.add(method); + return null; + } + + String normalizedJsReturn = jsReturnType; + if (normalizedJsReturn.contains("|")) { + normalizedJsReturn = normalizedJsReturn.split("\\|")[0].trim(); + } + normalizedJsReturn = normalizedJsReturn.replace("?", "").trim(); + + // Fast-path: skip resolver if the .d.ts return matches reflection (common case). + Class reflectedReturn = method.getReturnType(); + if (reflectedReturn != null) { + if (normalizedJsReturn.equals(reflectedReturn.getName()) + || normalizedJsReturn.equals(reflectedReturn.getSimpleName()) + || normalizedJsReturn.equals("Java." + reflectedReturn.getName())) { + RETURN_OVERRIDE_MISSES.add(method); + return null; + } + + String lower = normalizedJsReturn.toLowerCase(); + if (("void".equals(lower) && (reflectedReturn == void.class || reflectedReturn == Void.class)) + || ("boolean".equals(lower) && (reflectedReturn == boolean.class || reflectedReturn == Boolean.class)) + || ("string".equals(lower) && reflectedReturn == String.class) + || ("number".equals(lower) && (reflectedReturn == double.class || reflectedReturn == Double.class))) { + RETURN_OVERRIDE_MISSES.add(method); + return null; + } + } + + TypeResolver resolver = TypeResolver.getInstance(); + TypeInfo resolved = resolver.resolveJSType(normalizedJsReturn); + TypeInfo javaResolved = null; + + if (resolved != null) { + if (resolved.getJavaClass() != null) { + javaResolved = resolved; + } else if (resolved.isJSType() && resolved.getJSTypeInfo() != null) { + String javaFqn = resolved.getJSTypeInfo().getJavaFqn(); + if (javaFqn != null && !javaFqn.isEmpty()) { + javaResolved = resolver.resolveFullName(javaFqn); + } + } + } + + if (javaResolved == null || javaResolved.getJavaClass() == null) { + RETURN_OVERRIDE_MISSES.add(method); + return null; + } + + RETURN_OVERRIDE_CACHE.put(method, javaResolved); + return javaResolved; } public static JSMethodInfo findMatchingMethod(Method method, TypeInfo containingType) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 381b817d0..8520992f8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -741,6 +741,14 @@ private void extractMethodCallInfo(Token token) { return; } if (methodInfo != null && methodInfo.getJavaMethod() != null) { + TypeInfo methodReturnType = methodInfo.getReturnType(); + Class reflectedReturnType = methodInfo.getJavaMethod().getReturnType(); + if (methodReturnType != null && methodReturnType.getJavaClass() != null + && methodReturnType.getJavaClass() != reflectedReturnType) { + // Use MethodInfo when its return type differs from reflection (e.g., .d.ts override like IDBCPlayer -> IDBCAddon in npcdbc). + buildBasicMethodDeclaration(methodInfo, containingType); + return; + } buildMethodDeclaration(methodInfo.getJavaMethod(), containingType); extractJavadoc(methodInfo.getJavaMethod()); return; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 60987e8cc..325ca4b56 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -146,8 +146,26 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); int modifiers = method.getModifiers(); - // Try to find matching JSFieldInfo to bridge parameter names and docs + // Try to find matching JSMethodInfo to bridge parameter names/docs and allow return overrides JSMethodInfo jsMethod = DtsJavaBridge.findMatchingMethod(method, containingType); + if (jsMethod != null) { + // If .d.ts provides a more specific return type, use it for editor typing. + // (e.g., .d.ts override like IDBCPlayer -> IDBCAddon in npcdbc). + TypeInfo overrideReturnType = DtsJavaBridge.resolveReturnTypeOverride(method, containingType, jsMethod); + if (overrideReturnType != null && overrideReturnType.getJavaClass() != null) { + Class reflectedReturn = method.getReturnType(); + Class overrideClass = overrideReturnType.getJavaClass(); + if (reflectedReturn.isAssignableFrom(overrideClass)) { + if (reflectedReturn != overrideClass) { + returnType = overrideReturnType; + System.out.println("[DtsJavaBridge] Overrode return type for " + + method.getDeclaringClass().getName() + "." + name + + "(" + method.getParameterCount() + ") from " + + reflectedReturn.getName() + " to " + overrideClass.getName()); + } + } + } + } List params = new ArrayList<>(); Class[] paramTypes = method.getParameterTypes(); List jsParams = jsMethod != null ? jsMethod.getParameters() : null; From df0d46e63d8e79a970252492eb81ca753467d3dc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 03:48:18 +0200 Subject: [PATCH 293/337] Properly parsed enclosed namespace types like INpcEvent.UpdateEvent and added them to JSTypeRegistry.types --- .../interpreter/js_parser/JSTypeRegistry.java | 9 +++ .../js_parser/TypeScriptDefinitionParser.java | 60 +++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) 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 ab6b8f45b..0d2f8e596 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 @@ -333,6 +333,15 @@ public void registerType(JSTypeInfo type) { if (existingType != null) { mergeType(existingType, type, getCurrentSource()); + + // Preserve an addressable key for the incoming declared name. + // Some .d.ts parsing paths can encounter the same Java type via multiple textual names + // (e.g., due to nested namespaces). Even if we merge by @javaFqn, callers still expect + // lookups like "INpcEvent.DamagedEvent" to work. + if (!types.containsKey(fullName)) { + types.put(fullName, existingType); + typeOrigins.put(fullName, getCurrentSource()); + } return; } 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 70f1e2d7b..9d8bc367c 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 @@ -211,8 +211,20 @@ private void parseIndexFile(String content) { * Parse interface and class definitions from content. */ private void parseInterfaceFile(String content, String parentNamespace, String packageName) { + // IMPORTANT: + // Many generated .d.ts files declare nested exported interfaces inside `namespace X { ... }` blocks. + // If we scan `content` directly for `export interface` / `export class`, we'll accidentally treat + // those nested exports as top-level for the current `parentNamespace`. + // That pollutes the registry with incorrectly-scoped type names (e.g. "DamagedEvent" instead of + // "INpcEvent.DamagedEvent"), and later registry merges (by @javaFqn) can prevent the correctly + // namespaced key from ever being registered. + // + // To avoid this, we strip namespace blocks out of the scan input and then parse namespaces + // explicitly via the NAMESPACE_PATTERN recursion further below. + String scanContent = stripNamespaceBlocks(content); + // Find exported interfaces - Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(content); + Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(scanContent); while (interfaceMatcher.find()) { String interfaceName = interfaceMatcher.group(1); String typeParamsStr = interfaceMatcher.group(2); // e.g., "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP */" @@ -267,7 +279,7 @@ private void parseInterfaceFile(String content, String parentNamespace, String p } // Find nested interfaces (without export keyword) - Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content); + Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(scanContent); while (nestedInterfaceMatcher.find()) { String interfaceName = nestedInterfaceMatcher.group(1); String typeParamsStr = nestedInterfaceMatcher.group(2); @@ -307,7 +319,7 @@ private void parseInterfaceFile(String content, String parentNamespace, String p } // Find classes (same logic as interfaces) - Matcher classMatcher = CLASS_PATTERN.matcher(content); + Matcher classMatcher = CLASS_PATTERN.matcher(scanContent); while (classMatcher.find()) { String className = classMatcher.group(1); String typeParamsStr = classMatcher.group(2); @@ -361,7 +373,7 @@ private void parseInterfaceFile(String content, String parentNamespace, String p // Namespaces contain exported types, so use parseInterfaceFile String fullNamespace = parentNamespace != null ? parentNamespace + "." + namespaceName : namespaceName; - parseInterfaceFile(body, fullNamespace, packageName); + parseInterfaceFile(body, fullNamespace, packageName); // Don't call parseNestedTypes here - namespace members are all exported // and will be caught by parseInterfaceFile's INTERFACE_PATTERN } @@ -372,6 +384,46 @@ private void parseInterfaceFile(String content, String parentNamespace, String p parseTypeAliases(content, null); } } + + /** + * Strip namespace blocks from a file-level scan to avoid incorrectly capturing nested exports. + * + * This replaces the entire `namespace X { ... }` span (including braces) with whitespace so that + * match indices remain stable when we later slice `content` for bodies. + */ + private String stripNamespaceBlocks(String content) { + if (content == null || content.isEmpty()) { + return content; + } + + Matcher m = NAMESPACE_PATTERN.matcher(content); + if (!m.find()) { + return content; + } + + char[] chars = content.toCharArray(); + + int searchFrom = 0; + m.reset(); + while (m.find(searchFrom)) { + int start = m.start(); + int bodyStart = m.end(); + int bodyEnd = findMatchingBrace(content, bodyStart - 1); + if (bodyEnd <= start) { + searchFrom = Math.max(m.end(), searchFrom + 1); + continue; + } + + int endExclusive = Math.min(chars.length, bodyEnd + 1); + for (int i = start; i < endExclusive; i++) { + chars[i] = ' '; + } + + searchFrom = endExclusive; + } + + return new String(chars); + } /** * Parse type parameters from a string like "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP *`/". From aae2fdb23a3bff11efdfca9a35028f28c109e257 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 03:49:38 +0200 Subject: [PATCH 294/337] Added DataScript global definitions i.e. npc, world, event to syntax highlighter as global vars --- .../noppes/npcs/client/gui/GuiScript.java | 20 +++- .../client/gui/util/GuiScriptTextArea.java | 4 + .../script/interpreter/ScriptDocument.java | 44 +++++++++ .../interpreter/ScriptTextContainer.java | 20 +++- .../npcs/controllers/data/DataScript.java | 91 +++++++++++++++---- 5 files changed, 155 insertions(+), 24 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 8d960e142..bbbe4e1bf 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -16,6 +16,8 @@ import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextArea; import noppes.npcs.client.gui.util.GuiScriptTextArea; +import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; @@ -160,6 +162,10 @@ private void initScriptView() { activeArea.setLanguage(script.getLanguage()); activeArea.setScriptContext(getScriptContext()); + + // Set editor globals based on the active NPC hook + String hookName = EnumScriptType.values()[activeTab].function; + applyEditorGlobals(activeArea, hookName); // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { @@ -225,6 +231,19 @@ private void initSettingsView() { addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); } + // Apply editor globals for the active NPC hook. + private void applyEditorGlobals(GuiScriptTextArea activeArea, String hookName) { + if (activeArea == null) + return; + + ScriptTextContainer textContainer = activeArea.getContainer(); + if (textContainer == null) + return; + + if (script != null) + textContainer.setEditorGlobalsMap(script.getEditorGlobals(hookName)); + } + @Override protected String getConsoleText() { Map map = this.script.getOldConsoleText(); @@ -371,4 +390,3 @@ public void mouseClicked(int mouseX, int mouseY, int mouseButton) { super.mouseClicked(mouseX, mouseY, mouseButton); } } - 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 c8e3af3de..da994eb4f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2652,6 +2652,10 @@ public ScriptContext getScriptContext() { return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; } + public ScriptTextContainer getContainer() { + return this.container; + } + /** * Add implicit imports that should be resolved without explicit import statements. * Used for JaninoScript default imports and hook parameter types. 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 5a5e29ef6..8f8002e56 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 @@ -24,6 +24,7 @@ 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 noppes.npcs.controllers.data.DataScript; import java.util.*; import java.util.regex.Matcher; @@ -130,6 +131,9 @@ public class ScriptDocument { // Determines which hooks and event types are available private ScriptContext scriptContext = ScriptContext.GLOBAL; + // Editor-injected globals (name -> type name) provided by the handler + private Map editorGlobals = Collections.emptyMap(); + // Implicit imports from JaninoScript default imports and hook parameter types // These are types that should be resolved even without explicit import statements private final Set implicitImports = new HashSet<>(); @@ -198,6 +202,26 @@ public ScriptContext getScriptContext() { return scriptContext; } + /** + * Set editor-injected globals (name -> type name). + * i.e. DataScript global definitions {@link DataScript#getEditorGlobals(String)} + * This is provided by the script handler at editor init time. + */ + public void setEditorGlobals(Map globals) { + if (globals == null || globals.isEmpty()) { + this.editorGlobals = Collections.emptyMap(); + return; + } + this.editorGlobals = new LinkedHashMap<>(globals); + } + + /** + * Get editor-injected globals (name -> type name). + */ + public Map getEditorGlobals() { + return Collections.unmodifiableMap(editorGlobals); + } + /** * Add implicit imports that should be resolved without explicit import statements. * Used for JaninoScript default imports and hook parameter types. @@ -4688,6 +4712,16 @@ private void markVariables(List marks) { continue; } } + + // Mark DataScript global variable definitions + if (!editorGlobals.isEmpty() && editorGlobals.containsKey(name)) { + FieldInfo fieldInfo = resolveVariable(name, position); + if (fieldInfo != null) { + Object metadata = callInfo != null ? new FieldInfo.ArgInfo(fieldInfo, callInfo) : fieldInfo; + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD, metadata)); + continue; + } + } } // Skip uppercase if not a known field - type handling will deal with it @@ -5664,6 +5698,16 @@ public FieldInfo resolveVariable(String name, int position) { return FieldInfo.globalField(name, typeInfo, -1); } } + + // Check if is DataScript global variable definition + String editorGlobalType = editorGlobals.get(name); + if (editorGlobalType != null) { + TypeInfo typeInfo = resolveType(editorGlobalType); + if (typeInfo == null || !typeInfo.isResolved()) { + typeInfo = TypeInfo.ANY; + } + return FieldInfo.globalField(name, typeInfo, -1); + } } return null; 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 4b2e89e26..05da45227 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 @@ -2,12 +2,10 @@ 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; +import java.util.Collections; +import java.util.Map; /** * New interpreter-based text container that replaces JavaTextContainer. @@ -39,6 +37,9 @@ public class ScriptTextContainer extends JavaTextContainer { /** The script context: NPC, PLAYER, BLOCK, ITEM, etc. */ private ScriptContext scriptContext = ScriptContext.GLOBAL; + /** Editor-injected globals (name -> type name) */ + private Map editorGlobals = Collections.emptyMap(); + public ScriptTextContainer(String text) { super(text); if (USE_NEW_INTERPRETER) { @@ -94,6 +95,16 @@ public ScriptContext getScriptContext() { return scriptContext; } + /** + * Set editor-injected globals (name -> type name). + */ + public void setEditorGlobalsMap(java.util.Map globals) { + this.editorGlobals = globals != null ? globals : java.util.Collections.emptyMap(); + if (document != null) { + document.setEditorGlobals(this.editorGlobals); + } + } + /** * Add implicit imports that should be resolved without explicit import statements. * Used for JaninoScript default imports and hook parameter types. @@ -143,6 +154,7 @@ public void init(String text, int width, int height) { if (document == null) { document = new ScriptDocument(this.text, this.language); document.setScriptContext(this.scriptContext); + document.setEditorGlobals(this.editorGlobals); } init(width, height); diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 74806605f..fada88b04 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -20,6 +20,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.janino.EventJaninoScript; import noppes.npcs.scripted.NpcAPI; @@ -28,15 +29,10 @@ import noppes.npcs.scripted.constants.JobType; import noppes.npcs.scripted.constants.RoleType; import noppes.npcs.scripted.entity.ScriptNpc; +import noppes.npcs.api.handler.IHookDefinition; import javax.script.ScriptEngine; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; public class DataScript implements IScriptHandlerPacket { public List eventScripts = new ArrayList<>(); @@ -56,6 +52,17 @@ public class DataScript implements IScriptHandlerPacket { public boolean aiNeedsUpdate = false; public boolean hasInited = false; + // Editor/runtime globals descriptor used for a single source of truth. + private static final class GlobalDefinition { + private final String typeName; + private final Object value; + + private GlobalDefinition(String typeName, Object value) { + this.typeName = typeName; + this.value = value; + } + } + public DataScript(EntityNPCInterface npc) { for (int i = 0; i < 15; i++) { this.setNPCScript(i, new ScriptContainer(this)); @@ -180,21 +187,12 @@ public boolean callScript(EnumScriptType type, Event event, Object... obs) { LogWriter.postScriptLog(npc.field_110179_h, type, String.format("[%s] NPC %s (%s, %s, %s) | Objects: %s", ((String) type.function).toUpperCase(), npc.display.name, (int) npc.posX, (int) npc.posY, (int) npc.posZ, Arrays.toString(obs))); } } - return callScript(script, event); + return callScript(script, type.function, event); } - private boolean callScript(ScriptContainer script, Event event) { + private boolean callScript(ScriptContainer script, String hookName, Event event) { ScriptEngine engine = script.engine; - engine.put("npc", dummyNpc); - engine.put("world", dummyWorld); - engine.put("event", event); - engine.put("API", NpcAPI.Instance()); - engine.put("EntityType", entities); - engine.put("RoleType", roles); - engine.put("JobType", jobs); - for (Map.Entry engineObjects : NpcAPI.engineObjects.entrySet()) { - engine.put(engineObjects.getKey(), engineObjects.getValue()); - } + applyGlobalsToEngine(engine, hookName, event); script.run(engine); if (clientNeedsUpdate) { @@ -254,6 +252,61 @@ public void callScript(String hookName, Event event) { } } + public Map getEditorGlobals(String hookName) { + Map definitions = getGlobalDefinitions(hookName, null); + Map globals = new LinkedHashMap<>(); + for (Map.Entry entry : definitions.entrySet()) { + String typeName = entry.getValue().typeName; + if (typeName != null && !typeName.isEmpty()) { + globals.put(entry.getKey(), typeName); + } + } + return globals; + } + + // Apply runtime globals plus engine-only bindings. + private void applyGlobalsToEngine(ScriptEngine engine, String hookName, Event event) { + Map definitions = getGlobalDefinitions(hookName, event); + for (Map.Entry entry : definitions.entrySet()) { + engine.put(entry.getKey(), entry.getValue().value); + } + + engine.put("API", NpcAPI.Instance()); + for (Map.Entry engineObjects : NpcAPI.engineObjects.entrySet()) { + engine.put(engineObjects.getKey(), engineObjects.getValue()); + } + } + + // Build shared globals (editor types + runtime values). + private Map getGlobalDefinitions(String hookName, Event event) { + Map globals = new LinkedHashMap<>(); + globals.put("npc", new GlobalDefinition("ICustomNpc", dummyNpc)); + globals.put("world", new GlobalDefinition("IWorld", dummyWorld)); + globals.put("event", new GlobalDefinition(resolveEditorEventTypeName(hookName), event)); + globals.put("EntityType", new GlobalDefinition("noppes.npcs.scripted.constants.EntityType", entities)); + globals.put("RoleType", new GlobalDefinition("noppes.npcs.scripted.constants.RoleType", roles)); + globals.put("JobType", new GlobalDefinition("noppes.npcs.scripted.constants.JobType", jobs)); + + return globals; + } + + // Resolve editor-only event type from hook name with a single fallback. + private String resolveEditorEventTypeName(String hookName) { + final String fallback = "INpcEvent"; + if (hookName == null || hookName.isEmpty()) + return fallback; + + IHookDefinition definition = ScriptHookController.Instance.getHookDefinition(getContext().hookContext, hookName); + if (definition == null) + return fallback; + + String typeName = definition.getUsableTypeName(); + if (typeName == null || typeName.isEmpty()) + return fallback; + + return typeName; + } + public boolean isClient() { return this.npc.isRemote(); } From 76b6dae9d45fdfcc79810c1ae0abf5b1799bdca4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 03:50:33 +0200 Subject: [PATCH 295/337] Added API global engine objects and DataScript global definitions as JS autocomplete suggestions --- .../autocomplete/JSAutocompleteProvider.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index 641add7c0..7411eecd8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.autocomplete; import noppes.npcs.client.gui.util.script.interpreter.js_parser.*; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.*; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; @@ -155,6 +156,24 @@ protected void addUnimportedClassSuggestions(String prefix, List items) { + super.addLanguageUniqueSuggestions(context, items); + + // Add global variables from both global engine objects and document editor/DataScript globals + List globalNames = new ArrayList<>(); + globalNames.addAll(registry.getGlobalEngineObjects().keySet()); + globalNames.addAll(document.getEditorGlobals().keySet()); + + for (String name : globalNames) { + FieldInfo fieldInfo = document.resolveVariable(name, context.cursorPosition); + if (fieldInfo == null || !fieldInfo.isResolved()) + continue; + + items.add(AutocompleteItem.fromField(fieldInfo)); + } + } + protected UsageTracker getUsageTracker() { return UsageTracker.getJSInstance(); } From bd1743d0694099b3a697292ed94e5acd68d5a6fd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 03:51:02 +0200 Subject: [PATCH 296/337] Penalised keyword items in autocomplete suggestions --- .../autocomplete/JavaAutocompleteProvider.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 9a871afbb..81e105b4b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -368,6 +368,7 @@ protected void filterAndScore(List items, String prefix, applyUsageBoost(item, tracker, ownerFullName); applyStaticPenalty(item, isMemberAccess, isStaticContext); applyObjectMethodPenalty(item); + applyKeywordPenalty(item, prefix); } return; } @@ -387,6 +388,7 @@ protected void filterAndScore(List items, String prefix, applyUsageBoost(item, tracker, ownerFullName); applyStaticPenalty(item, isMemberAccess, isStaticContext); applyObjectMethodPenalty(item); + applyKeywordPenalty(item, prefix); } } } @@ -454,6 +456,19 @@ protected void applyObjectMethodPenalty(AutocompleteItem item) { item.addScoreBoost(-10000); } } + + /** + * Push keywords below non-keywords unless user typed a longer prefix. + */ + protected void applyKeywordPenalty(AutocompleteItem item, String prefix) { + if (item.getKind() != AutocompleteItem.Kind.KEYWORD) { + return; + } + + if (prefix == null || prefix.length() < 2) { + item.addScoreBoost(-10000); + } + } /** * Builder class for AutocompleteItem to handle cases without source data. From 3f204d412dd662478a8682dbacb7816c81d228a5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 04:00:24 +0200 Subject: [PATCH 297/337] Removed redundant fullScreenButton.draw calls, already drawn in GuiScriptTextArea --- .../java/noppes/npcs/client/gui/GuiScript.java | 10 ---------- .../client/gui/script/GuiScriptInterface.java | 15 +-------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index bbbe4e1bf..a289761b0 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -371,16 +371,6 @@ public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { } } - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - super.drawScreen(mouseX, mouseY, partialTicks); - - // Draw fullscreen button when in script view (GuiScript uses 0-based activeTab) - if (showScript) { - fullscreenButton.draw(mouseX, mouseY); - } - } - @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { // Check fullscreen button first when in script view 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 982bf4aea..40cfe103f 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -380,20 +380,7 @@ public GuiScriptInterface setDimensions(int x, int y) { protected ScriptContext getScriptContext() { return handler.getContext(); } - - // ==================== RENDERING ==================== - - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { - super.drawScreen(mouseX, mouseY, partialTicks); - - // Draw fullscreen button on top of everything when on script editor tab - // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves - if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer()) { - fullscreenButton.draw(mouseX, mouseY); - } - } - + // ==================== MOUSE HANDLING ==================== @Override From 8fa22e1e60992eb0ba904638cfe6d570642702dc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 04:56:42 +0200 Subject: [PATCH 298/337] Moved fullscreen button mouseClicked into GuiScriptTextArea --- src/main/java/noppes/npcs/client/gui/GuiScript.java | 9 --------- .../npcs/client/gui/script/GuiScriptInterface.java | 6 ------ .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++++ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index a289761b0..d7ea08e54 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -370,13 +370,4 @@ public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { initGui(); } } - - @Override - public void mouseClicked(int mouseX, int mouseY, int mouseButton) { - // Check fullscreen button first when in script view - if (showScript && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { - return; - } - super.mouseClicked(mouseX, mouseY, mouseButton); - } } 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 40cfe103f..f461c66f6 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -394,12 +394,6 @@ public void mouseClicked(int mouseX, int mouseY, int mouseButton) { return; } - // Check fullscreen button first when on script editor tab - // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves - if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer() && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { - return; - } - super.mouseClicked(mouseX, mouseY, mouseButton); } 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 da994eb4f..e3067a5e7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2429,6 +2429,10 @@ public void mouseClicked(int xMouse, int yMouse, int mouseButton) { if (autocompleteManager.isVisible()) { autocompleteManager.dismiss(); } + + if (parent != null && parent.fullscreenButton.mouseClicked(xMouse, yMouse, mouseButton)) + return; + // Check go to line dialog clicks first if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { From ac8228e4d168c79969b4e8f395dad2bd806e087d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 05:12:02 +0200 Subject: [PATCH 299/337] Properly ordered mouse clicked components --- .../noppes/npcs/client/gui/util/GuiNPCInterface.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 43c4a17f3..9cefa6816 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -153,9 +153,8 @@ public void mouseClicked(int i, int j, int k) { if (subgui != null) subgui.mouseClicked(i, j, k); else { - for (GuiNpcTextField tf : new ArrayList(textfields.values())) - if (tf.enabled) - tf.mouseClicked(i, j, k); + mouseEvent(i, j, k); + vanillaMouseClicked(i, j, k); for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) { guiScrollableComponent.mouseClicked(i, j, k); @@ -173,8 +172,11 @@ public void mouseClicked(int i, int j, int k) { return; } } - mouseEvent(i, j, k); - vanillaMouseClicked(i, j, k); + + for (GuiNpcTextField tf : new ArrayList(textfields.values())) + if (tf.enabled) + tf.mouseClicked(i, j, k); + } } From 4245f5bbbc03f35fb9147659eaecd68be599feb1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 05:18:09 +0200 Subject: [PATCH 300/337] Improved fullscreen mode for legacy GuiScript --- .../noppes/npcs/client/gui/GuiScript.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index d7ea08e54..796064b60 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -90,18 +90,25 @@ public void initGui() { super.initGui(); this.guiTop += 10; - // ==================== TOP BUTTONS (hidden in fullscreen) ==================== - if (!isFullscreen) { - GuiMenuTopButton top; - addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); - addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); - addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); - top.active = showScript; - addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); - top.active = !showScript; - addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + if (isFullscreen) { + FullscreenConfig.paddingTop = 30; } + // ==================== TOP BUTTONS ==================== + boolean isFullscreenView = isFullscreen && showScript; + int menuX = isFullscreenView ? FullscreenConfig.paddingLeft : guiLeft + 4; + int menuY = isFullscreenView ? FullscreenConfig.paddingTop - 20 : guiTop - 17; + int rightX = isFullscreenView ? width - FullscreenConfig.paddingRight : guiLeft + xSize; + + GuiMenuTopButton top; + addTopButton(top = new GuiMenuTopButton(13, menuX, menuY, "script.scripts")); + addTopButton(new GuiMenuTopButton(16, rightX - 102, menuY, "eventscript.eventScripts")); + addTopButton(new GuiMenuTopButton(17, rightX - 22, menuY, "X")); + top.active = showScript; + addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); + top.active = !showScript; + addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + if (showScript) { initScriptView(); } else { From 6f1fc08a088a1443d9796bf725078c0feb7e9822 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:00:36 +0200 Subject: [PATCH 301/337] init'd JSTypeRegistry on game load --- src/main/java/noppes/npcs/client/ClientProxy.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 5d95560b8..6a65e71b6 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -131,6 +131,7 @@ import noppes.npcs.client.gui.script.GuiScriptGlobal; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.script.PackageFinder; +import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.type.ClassIndex; import noppes.npcs.client.model.ModelNPCGolem; import noppes.npcs.client.model.ModelNpcCrystal; @@ -786,6 +787,7 @@ public void buildPackageIndex() { try { PackageFinder.init(Thread.currentThread().getContextClassLoader()); ClassIndex.init(); + JSTypeRegistry.getInstance().initializeFromResources(); } catch (IOException e) { } } From 720d0dc1b90a274d6304a2fe31f2a1062ffae103 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:01:54 +0200 Subject: [PATCH 302/337] On Autocomplete insertion, set cursor carat inside () if method has parameters --- .../npcs/client/gui/util/GuiScriptTextArea.java | 6 ++++++ .../script/autocomplete/AutocompleteManager.java | 12 ++++++++++++ 2 files changed, 18 insertions(+) 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 e3067a5e7..0972872ef 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -523,6 +523,12 @@ public int getCursorPosition() { return selection.getCursorPosition(); } + @Override + public void setCursorPosition(int position) { + selection.reset(Math.max(0, Math.min(position, GuiScriptTextArea.this.text.length()))); + scrollToCursor(); + } + @Override public String getText() { return GuiScriptTextArea.this.text; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 1996f2cec..ca2cde68c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -91,6 +91,12 @@ public interface InsertCallback { */ int getCursorPosition(); + /** + * Set cursor position. + * @param position New cursor position + */ + void setCursorPosition(int position); + /** * Get current document text. */ @@ -543,6 +549,12 @@ private void handleItemSelected(AutocompleteItem item) { // This prevents position corruption since import adds text at the TOP insertCallback.replaceTextRange(insertText, prefixStartPosition, endPos); + // If the inserted text ends with (), move cursor inside only when parameters exist + if (insertText.endsWith("()") && item.getParameterCount() > 0) { + int currentCursor = insertCallback.getCursorPosition(); + insertCallback.setCursorPosition(currentCursor - 1); + } + // If this item requires an import, add it AFTER the text replacement if (item.requiresImport() && item.getImportPath() != null) { insertCallback.addImport(item.getImportPath()); From 940dc01a7cf1b2d6765867617ffddb8c8498385a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:05:32 +0200 Subject: [PATCH 303/337] DELETE_LINE, MOVE_LINE_UP and MOVE_LINE_DOWN KeyPresets --- .../client/gui/util/GuiScriptTextArea.java | 136 ++++++++++++++++++ .../client/key/impl/ScriptEditorKeys.java | 5 + 2 files changed, 141 insertions(+) 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 0972872ef..9a4ad2b58 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1390,6 +1390,142 @@ private void initializeKeyBindings() { } }); + // DELETE_LINE: Delete the current line + KEYS.DELETE_LINE.setTask(e -> { + if (!e.isPress() || !isActive.get()) + return; + + if (text == null || text.isEmpty()) + return; + + int cursor = selection.getCursorPosition(); + + LineData targetLine = null; + int targetIndex = -1; + for (LineData line : container.lines) { + if (cursor >= line.start && cursor <= line.end) { + targetLine = line; + targetIndex = container.lines.indexOf(line); + break; + } + } + + if (targetLine == null) + return; + + int start = Math.max(0, Math.min(targetLine.start, text.length())); + int end = Math.max(start, Math.min(targetLine.end, text.length())); + + String newText = text.substring(0, start) + text.substring(end); + setText(newText); + + int newCursor = 0; + if (targetIndex > 0 && targetIndex <= container.lines.size() - 1) { + LineData previousLine = container.lines.get(targetIndex - 1); + int prevStart = Math.max(0, Math.min(previousLine.start, newText.length())); + int prevEnd = Math.max(prevStart, Math.min(previousLine.end, newText.length())); + int prevLen = Math.max(0, prevEnd - prevStart); + int offsetInLine = Math.max(0, Math.min(cursor - targetLine.start, prevLen)); + newCursor = Math.min(prevStart + offsetInLine, newText.length()); + } + + selection.reset(Math.min(newCursor, newText.length())); + scrollToCursor(); + }); + + // MOVE_LINE_UP: Move current line up + KEYS.MOVE_LINE_UP.setThrottleInterval(50).setTask(e -> { + if ((!e.isPress() && !e.isHold()) || !isActive.get()) + return; + + if (text == null || text.isEmpty()) + return; + + int tLen = text.length(); + int cursor = Math.max(0, Math.min(selection.getCursorPosition(), tLen)); + + // Work on real newline-delimited lines, not wrapped LineData. + int currStart = text.lastIndexOf('\n', Math.max(0, cursor - 1)); + currStart = currStart == -1 ? 0 : (currStart + 1); + if (currStart == 0) + return; // Can't move first line up + + int currEnd = text.indexOf('\n', cursor); + currEnd = currEnd == -1 ? tLen : (currEnd + 1); + + int prevEnd = currStart; + int prevStart = text.lastIndexOf('\n', Math.max(0, prevEnd - 2)); + prevStart = prevStart == -1 ? 0 : (prevStart + 1); + + String previousText = text.substring(prevStart, prevEnd); + String currentText = text.substring(currStart, currEnd); + + // If current line is the last line without a trailing newline, but it is being moved + // into the middle, ensure it ends with '\n' by transferring the '\n' from the previous line. + if (!currentText.endsWith("\n") && previousText.endsWith("\n") && !previousText.isEmpty()) { + currentText = currentText + "\n"; + previousText = previousText.substring(0, previousText.length() - 1); + } + + String before = text.substring(0, prevStart); + String after = text.substring(currEnd); + String newText = before + currentText + previousText + after; + setText(newText); + + int currentContentLen = currentText.endsWith("\n") ? Math.max(0, currentText.length() - 1) : currentText.length(); + int offsetInLine = Math.max(0, Math.min(cursor - currStart, currentContentLen)); + int newCursor = Math.min(prevStart + offsetInLine, newText.length()); + selection.reset(newCursor); + scrollToCursor(); + }); + + // MOVE_LINE_DOWN: Move current line down + KEYS.MOVE_LINE_DOWN.setThrottleInterval(50).setTask(e -> { + if ((!e.isPress() && !e.isHold()) || !isActive.get()) + return; + + if (text == null || text.isEmpty()) + return; + + int tLen = text.length(); + int cursor = Math.max(0, Math.min(selection.getCursorPosition(), tLen)); + + // Work on real newline-delimited lines, not wrapped LineData. + int currStart = text.lastIndexOf('\n', Math.max(0, cursor - 1)); + currStart = currStart == -1 ? 0 : (currStart + 1); + + int currEnd = text.indexOf('\n', cursor); + currEnd = currEnd == -1 ? tLen : (currEnd + 1); + if (currEnd >= tLen) + return; // Can't move last line down + + int nextStart = currEnd; + int nextEnd = text.indexOf('\n', nextStart); + nextEnd = nextEnd == -1 ? tLen : (nextEnd + 1); + + String currentText = text.substring(currStart, currEnd); + String nextText = text.substring(nextStart, nextEnd); + + // If the next line is the last line and doesn't end with '\n' (common when it contains + // only indentation spaces), swapping can merge whitespace onto the moved line. + // Fix by transferring the '\n' from currentText to nextText. + if (!nextText.endsWith("\n") && currentText.endsWith("\n") && !currentText.isEmpty()) { + currentText = currentText.substring(0, currentText.length() - 1); + nextText = nextText + "\n"; + } + + String before = text.substring(0, currStart); + String after = text.substring(nextEnd); + String newText = before + nextText + currentText + after; + setText(newText); + + int currentContentLen = currentText.endsWith("\n") ? Math.max(0, currentText.length() - 1) : currentText.length(); + int offsetInLine = Math.max(0, Math.min(cursor - currStart, currentContentLen)); + int newCursor = Math.min(currStart + nextText.length() + offsetInLine, newText.length()); + selection.reset(newCursor); + scrollToCursor(); + }); + // Check if can open just for SearchReplaceBar and GoToLine diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index fd8a13098..3029d4424 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -18,6 +18,11 @@ public class ScriptEditorKeys extends KeyPresetManager { public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, false, true, false); public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false); + // Editing + public final KeyPreset DELETE_LINE = add("Delete Line").setDefaultState(Keyboard.KEY_DELETE, true, false, false); + public final KeyPreset MOVE_LINE_UP = add("Move Line Up").setDefaultState(Keyboard.KEY_UP, false, true, false); + public final KeyPreset MOVE_LINE_DOWN = add("Move Line Down").setDefaultState(Keyboard.KEY_DOWN, false, true, false); + // Search/Replace public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_F, true, false, false); public final KeyPreset SEARCH_REPLACE = add("Replace").setDefaultState(Keyboard.KEY_F, true, false, true); From 8b5d3a20777ce60b611a0dec0851f58c5de4e787 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:08:06 +0200 Subject: [PATCH 304/337] Copy with no selection now copies whole line, and pastes to a new line --- .../client/gui/util/GuiScriptTextArea.java | 71 ++++++++++++++++++- 1 file changed, 68 insertions(+), 3 deletions(-) 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 9a4ad2b58..7fba3526e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -111,6 +111,10 @@ private int getPaddedLineCount() { public List redoList = new ArrayList<>(); public boolean undoing = false; + // Clipboard tracking for line-copy paste behavior + private boolean lastCopyWasLine = false; + private String lastCopiedLineText = null; + // ==================== KEYS ==================== public static final ScriptEditorKeys KEYS = new ScriptEditorKeys(); public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS); @@ -1263,13 +1267,35 @@ private void initializeKeyBindings() { } }); - // COPY: Copy selection to clipboard + // COPY: Copy selection to clipboard (or entire line if no selection) KEYS.COPY.setTask(e -> { if (!e.isPress() || !isActive.get()) return; - if (selection.hasSelection()) + if (selection.hasSelection()) { NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + lastCopyWasLine = false; + lastCopiedLineText = null; + } else { + // Copy entire current line (including newline) + int cursor = selection.getCursorPosition(); + LineData targetLine = null; + for (LineData line : container.lines) { + if (cursor >= line.start && cursor <= line.end) { + targetLine = line; + break; + } + } + + if (targetLine != null) { + int safeStart = Math.max(0, Math.min(targetLine.start, text.length())); + int safeEnd = Math.max(safeStart, Math.min(targetLine.end, text.length())); + String lineText = text.substring(safeStart, safeEnd); + NoppesStringUtils.setClipboardContents(lineText); + lastCopyWasLine = true; + lastCopiedLineText = lineText; + } + } }); // PASTE: Insert clipboard contents at caret @@ -1277,7 +1303,46 @@ private void initializeKeyBindings() { if (!e.isPress() || !isActive.get()) return; - addText(NoppesStringUtils.getClipboardContents()); + String clipboard = NoppesStringUtils.getClipboardContents(); + if (clipboard == null) + clipboard = ""; + + if (selection.hasSelection()) { + addText(clipboard); + lastCopyWasLine = false; + lastCopiedLineText = null; + scrollToCursor(); + return; + } + + boolean isLinePaste = lastCopyWasLine && lastCopiedLineText != null && clipboard.equals(lastCopiedLineText); + if (isLinePaste && container != null && container.lines != null) { + LineData currentLine = selection.findCurrentLine(container.lines); + if (currentLine != null) { + int insertPos = Math.max(0, Math.min(currentLine.end, text.length())); + String insertText = clipboard; + + // Ensure insertion happens on the line below + if (insertPos > 0 && text.charAt(insertPos - 1) != '\n') { + insertText = "\n" + insertText; + } + + // Ensure the inserted line doesn't merge with the following line + if (insertPos < text.length() && !insertText.endsWith("\n")) { + insertText = insertText + "\n"; + } + + String newText = text.substring(0, insertPos) + insertText + text.substring(insertPos); + setText(newText); + + int newCursor = insertPos + (insertText.startsWith("\n") ? 1 : 0); + selection.reset(Math.min(newCursor, newText.length())); + scrollToCursor(); + return; + } + } + + addText(clipboard); scrollToCursor(); }); From 1863038019d50317d0a03c24980a76fea41f69a2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:09:00 +0200 Subject: [PATCH 305/337] Atomic UNDO/REDO for all typed text (operates on entire words at once, not just characters) --- .../client/gui/util/GuiScriptTextArea.java | 80 +++++++++++++++---- 1 file changed, 66 insertions(+), 14 deletions(-) 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 7fba3526e..093bd0e8d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -14,6 +14,9 @@ import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteMenu; import noppes.npcs.client.gui.util.script.interpreter.ScriptLine; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.token.Token; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; @@ -21,6 +24,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.gui.util.script.interpreter.type.ScriptTypeInfo; import noppes.npcs.client.key.impl.ScriptEditorKeys; import noppes.npcs.constants.ScriptContext; import noppes.npcs.util.ValueUtil; @@ -110,6 +114,10 @@ private int getPaddedLineCount() { public List undoList = new ArrayList<>(); public List redoList = new ArrayList<>(); public boolean undoing = false; + + // Atomic undo: group typing into word-based undo steps + private long lastTypingTime = 0; + private int lastTypingPos = -1; // Clipboard tracking for line-copy paste behavior private boolean lastCopyWasLine = false; @@ -2036,7 +2044,7 @@ private boolean handleDeletionKeys(int i) { if (!s.isEmpty() && !selection.hasSelection()) // remove single character after caret when nothing is selected s = s.substring(1); - setText(getSelectionBeforeText() + s); + setText(getSelectionBeforeText() + s, true); // Use atomic undo // Keep caret at same start selection selection.reset(selection.getStartSelection()); return true; @@ -2070,7 +2078,7 @@ private boolean handleDeletionKeys(int i) { before = before.substring(0, g); selection.setStartSelection(selection.getStartSelection() - (pos - g)); } - setText(before + getSelectionAfterText()); + setText(before + getSelectionAfterText(), true); // Use atomic undo selection.reset(selection.getStartSelection()); return true; } @@ -2085,7 +2093,7 @@ private boolean handleDeletionKeys(int i) { // 1) selection deletion if (selection.hasSelection()) { String s = getSelectionBeforeText(); - setText(s + getSelectionAfterText()); + setText(s + getSelectionAfterText(), true); // Use atomic undo selection.reset(selection.getStartSelection()); scrollToCursor(); return true; @@ -2109,7 +2117,7 @@ private boolean handleDeletionKeys(int i) { } String before = text.substring(0, ValueUtil.clamp(currCheck.start - 1, 0, text.length())); String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; - setText(before + after); + setText(before + after, true); // Use atomic undo int newCursor = Math.max(0, currCheck.start - 1); selection.reset(newCursor); scrollToCursor(); @@ -2138,7 +2146,7 @@ private boolean handleDeletionKeys(int i) { } String before = text.substring(0, curr.start); String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; - setText(before + after); + setText(before + after, true); // Use atomic undo // Place caret at end of previous line int newCursor = Math.max(0, curr.start - 1); selection.reset(newCursor); @@ -2165,7 +2173,7 @@ private boolean handleDeletionKeys(int i) { } } - setText(before + spacer + content); + setText(before + spacer + content, true); // Use atomic undo int newCursor = before.length() + spacer.length(); selection.reset(newCursor); scrollToCursor(); @@ -2187,7 +2195,7 @@ private boolean handleDeletionKeys(int i) { String before = text.substring(0, selection.getStartSelection() - 1); String after = selection.getStartSelection() + 1 < text.length() ? text.substring( selection.getStartSelection() + 1) : ""; - setText(before + after); + setText(before + after, true); // Use atomic undo selection.setStartSelection(selection.getStartSelection() - 1); selection.reset(selection.getStartSelection()); scrollToCursor(); @@ -2199,7 +2207,7 @@ private boolean handleDeletionKeys(int i) { String s = getSelectionBeforeText(); s = s.substring(0, s.length() - 1); selection.setStartSelection(selection.getStartSelection() - 1); - setText(s + getSelectionAfterText()); + setText(s + getSelectionAfterText(), true); // Use atomic undo selection.reset(selection.getStartSelection()); scrollToCursor(); // Notify autocomplete of deletion @@ -2366,7 +2374,7 @@ private boolean handleCharacterInput(char c) { // Auto-pair insertion: when opening a quote/brace/bracket is typed, // insert a matching closer and place caret between the pair. if (c == '"') { - setText(before + "\"\"" + after); + setText(before + "\"\"" + after, true); selection.reset(before.length() + 1); scrollToCursor(); // Notify autocomplete of the character @@ -2374,7 +2382,7 @@ private boolean handleCharacterInput(char c) { return true; } if (c == '\'') { - setText(before + "''" + after); + setText(before + "''" + after, true); selection.reset(before.length() + 1); scrollToCursor(); // Notify autocomplete of the character @@ -2382,7 +2390,7 @@ private boolean handleCharacterInput(char c) { return true; } if (c == '[') { - setText(before + "[]" + after); + setText(before + "[]" + after, true); selection.reset(before.length() + 1); scrollToCursor(); // Notify autocomplete of the character @@ -2390,7 +2398,7 @@ private boolean handleCharacterInput(char c) { return true; } if (c == '(') { - setText(before + "()" + after); + setText(before + "()" + after, true); selection.reset(before.length() + 1); scrollToCursor(); // Notify autocomplete of the character @@ -2604,7 +2612,7 @@ private void setCursor(int i, boolean select) { private void addText(String s) { int insertPos = selection.getStartSelection(); - this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText()); + this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText(), true); // Use atomic undo for typing selection.afterTextInsert(insertPos + s.length()); } @@ -2761,6 +2769,16 @@ public void updateCursorCounter() { // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { + setText(text, false); + } + + /** + * Set text with optional atomic undo support. + * + * @param text The new text content + * @param allowAtomic If true, allows grouping consecutive typing into single undo steps + */ + private void setText(String text, boolean allowAtomic) { if (text == null) { return; } @@ -2774,9 +2792,43 @@ public void setText(String text) { this.listener.textUpdate(text); } - if (!this.undoing) { + // Atomic undo logic: group consecutive typing into word-based undo steps + if (!this.undoing && allowAtomic && !undoList.isEmpty()) { + long now = System.currentTimeMillis(); + int cursorPos = selection.getCursorPosition(); + + // Check if we should merge with the previous undo entry + boolean shouldMerge = + (now - lastTypingTime < 2000) && // Within 2 seconds + Math.abs(cursorPos - lastTypingPos) <= 1; // Contiguous edit + + // If typing a space or newline, break the undo group + if (shouldMerge && text.length() > 0 && cursorPos > 0 && cursorPos <= text.length()) { + char lastChar = text.charAt(cursorPos - 1); + if (Character.isWhitespace(lastChar)) { + shouldMerge = false; + } + } + + if (shouldMerge) { + // Don't add a new undo entry - just update the text + // The existing undo entry will remain unchanged + } else { + // Add new undo entry + this.undoList.add(new GuiScriptTextArea.UndoData(this.text, selection.getCursorPosition())); + this.redoList.clear(); + } + + lastTypingTime = now; + lastTypingPos = cursorPos; + } else if (!this.undoing) { + // Normal undo entry (non-atomic) this.undoList.add(new GuiScriptTextArea.UndoData(this.text, selection.getCursorPosition())); this.redoList.clear(); + + // Reset atomic undo tracking + lastTypingTime = System.currentTimeMillis(); + lastTypingPos = selection.getCursorPosition(); } this.text = text; From ce7566e49843c6c5fd8e2330ef899db4a28353ae Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:09:40 +0200 Subject: [PATCH 306/337] CTRL + Click on fields/methods/type to go to definition --- .../client/gui/util/GuiScriptTextArea.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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 093bd0e8d..8489d5753 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2671,6 +2671,63 @@ public void mouseClicked(int xMouse, int yMouse, int mouseButton) { // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { + // Ctrl+Click: Go to definition + if (mouseButton == 0 && isCtrlKeyDown()) { + Object[] tokenInfo = getTokenAtScreenPosition(xMouse, yMouse); + if (tokenInfo != null) { + Token token = (Token) tokenInfo[0]; + int targetOffset = -1; + + // Check if token has method info with declaration + if (token.getMethodInfo() != null) { + MethodInfo methodInfo = token.getMethodInfo(); + if (methodInfo.getNameOffset() >= 0) { + targetOffset = methodInfo.getNameOffset(); + } + } + // Check if token has method call info with resolved declaration + else if (token.getMethodCallInfo() != null) { + MethodCallInfo callInfo = token.getMethodCallInfo(); + MethodInfo resolvedMethod = callInfo.getResolvedMethod(); + if (resolvedMethod != null && resolvedMethod.getNameOffset() >= 0) { + targetOffset = resolvedMethod.getNameOffset(); + } + } + // Check if token has field info with declaration + else if (token.getFieldInfo() != null) { + FieldInfo fieldInfo = token.getFieldInfo(); + if (fieldInfo.getDeclarationOffset() >= 0) { + targetOffset = fieldInfo.getDeclarationOffset(); + } + } + // Check if token has field access info with resolved declaration + else if (token.getFieldAccessInfo() != null) { + FieldAccessInfo accessInfo = token.getFieldAccessInfo(); + FieldInfo resolvedField = accessInfo.getResolvedField(); + if (resolvedField != null && resolvedField.getDeclarationOffset() >= 0) { + targetOffset = resolvedField.getDeclarationOffset(); + } + } + // Check if token is a script-defined type + else if (token.getTypeInfo() != null && token.getTypeInfo() instanceof ScriptTypeInfo) { + ScriptTypeInfo scriptType = + (ScriptTypeInfo) token.getTypeInfo(); + if (scriptType.getDeclarationOffset() >= 0) { + targetOffset = scriptType.getDeclarationOffset(); + } + } + + // Jump to definition if found + if (targetOffset >= 0) { + selection.reset(targetOffset); + scrollToCursor(); + this.clicked = false; + activeTextfield = this; + return; // Consume the event + } + } + } + // Compute logical click position in text int clickPos = this.getSelectionPos(xMouse, yMouse); From 83a54c10b85d73f623ea4d711a7586d039ef6317 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:10:27 +0200 Subject: [PATCH 307/337] forgot this from autocomplete insertion carat between () commit --- .../client/gui/util/script/autocomplete/AutocompleteItem.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index 4dcfa90e2..cf1201769 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -558,6 +558,8 @@ public int getParameterCount() { return ((MethodInfo) sourceData).getParameterCount(); } else if (sourceData instanceof JSMethodInfo) { return ((JSMethodInfo) sourceData).getParameterCount(); + } else if (sourceData instanceof SyntheticMethod) { + return ((SyntheticMethod) sourceData).parameters.size(); } return 0; } From ea8cc51eefe6543ca6074b5ba65c854f0cbc0321 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 19:19:58 +0200 Subject: [PATCH 308/337] Prevented auto pair insertion of '', "", [], () blocks in excluded ranges (comments,strings) --- .../client/gui/util/GuiScriptTextArea.java | 71 ++++++++++--------- .../script/interpreter/ScriptDocument.java | 3 +- 2 files changed, 39 insertions(+), 35 deletions(-) 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 8489d5753..977b213b9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2357,6 +2357,7 @@ private boolean handleCharacterInput(char c) { if (ChatAllowedCharacters.isAllowedCharacter(c)) { String before = getSelectionBeforeText(); String after = getSelectionAfterText(); + int cursorPos = selection.getCursorPosition(); // If the user types a closing character and that same closer is // already immediately after the caret, move caret past it instead @@ -2367,50 +2368,52 @@ private boolean handleCharacterInput(char c) { selection.reset(before.length() + 1); scrollToCursor(); // Notify autocomplete of the character - autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); + autocompleteManager.onCharTyped(c, text, cursorPos); return true; } // Auto-pair insertion: when opening a quote/brace/bracket is typed, // insert a matching closer and place caret between the pair. - if (c == '"') { - setText(before + "\"\"" + after, true); - selection.reset(before.length() + 1); - scrollToCursor(); - // Notify autocomplete of the character - autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); - return true; - } - if (c == '\'') { - setText(before + "''" + after, true); - selection.reset(before.length() + 1); - scrollToCursor(); - // Notify autocomplete of the character - autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); - return true; - } - if (c == '[') { - setText(before + "[]" + after, true); - selection.reset(before.length() + 1); - scrollToCursor(); - // Notify autocomplete of the character - autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); - return true; - } - if (c == '(') { - setText(before + "()" + after, true); - selection.reset(before.length() + 1); - scrollToCursor(); - // Notify autocomplete of the character - autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); - return true; + // But only if the current position is not excluded (e.g., inside a comment or string) + if (!container.getDocument().isExcluded(cursorPos)) { + if (c == '"') { + setText(before + "\"\"" + after, true); + selection.reset(before.length() + 1); + scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, cursorPos); + return true; + } + if (c == '\'') { + setText(before + "''" + after, true); + selection.reset(before.length() + 1); + scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, cursorPos); + return true; + } + if (c == '[') { + setText(before + "[]" + after, true); + selection.reset(before.length() + 1); + scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, cursorPos); + return true; + } + if (c == '(') { + setText(before + "()" + after, true); + selection.reset(before.length() + 1); + scrollToCursor(); + // Notify autocomplete of the character + autocompleteManager.onCharTyped(c, text, cursorPos); + return true; + } } - // Default insertion for printable characters: insert at caret (replacing selection) addText(Character.toString(c)); scrollToCursor(); // Notify autocomplete of the character - autocompleteManager.onCharTyped(c, text, selection.getCursorPosition()); + autocompleteManager.onCharTyped(c, text, cursorPos); return true; } return false; 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 8f8002e56..74f69a175 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 @@ -442,9 +442,10 @@ private void mergeOverlappingRanges() { excludedRanges.addAll(merged); } + public boolean isExcluded(int position) { for (int[] range : excludedRanges) { - if (position >= range[0] && position < range[1]) { + if (position >= range[0] && position <= range[1]) { return true; } } From 235fd3b3f31a9af8349c6010ee12826815ac9cef Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 21:52:04 +0200 Subject: [PATCH 309/337] Improved comment/string excluded range detection for auto-pair insertion --- .../npcs/client/gui/util/GuiScriptTextArea.java | 2 +- .../gui/util/script/interpreter/ScriptDocument.java | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) 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 977b213b9..3c1f40850 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2375,7 +2375,7 @@ private boolean handleCharacterInput(char c) { // Auto-pair insertion: when opening a quote/brace/bracket is typed, // insert a matching closer and place caret between the pair. // But only if the current position is not excluded (e.g., inside a comment or string) - if (!container.getDocument().isExcluded(cursorPos)) { + if (!container.getDocument().isExcludedInclusive(cursorPos)) { if (c == '"') { setText(before + "\"\"" + after, true); selection.reset(before.length() + 1); 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 74f69a175..a571f9576 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 @@ -444,6 +444,17 @@ private void mergeOverlappingRanges() { public boolean isExcluded(int position) { + for (int[] range : excludedRanges) { + if (position >= range[0] && position < range[1]) { + return true; + } + } + return false; + } + + /** Inclusive version for precision with end offset + Check GuiScriptTextArea#handleCharacterInput */ + public boolean isExcludedInclusive(int position) { for (int[] range : excludedRanges) { if (position >= range[0] && position <= range[1]) { return true; @@ -4739,7 +4750,7 @@ private void markVariables(List marks) { /** * Mark chained field accesses like: mc.player.world, array.length, this.field, etc. * This handles dot-separated access chains and colors each segment appropriately. - * Does NOT mark method calls (identifiers followed by parentheses) - those are handled by markMethodCalls. + * Does NOT mark method calls (identifiers followed by parentheses) - those are handled by {@link #markMethodCalls}. */ private void markChainedFieldAccesses(List marks) { FieldChainMarker marker = new FieldChainMarker(this, text); From 1e96a21cca2580149e66d51b19bd2de3c7e937b9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 26 Jan 2026 22:54:40 +0200 Subject: [PATCH 310/337] Added JS overloads to autocomplete properly --- .../autocomplete/JSAutocompleteProvider.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index 7411eecd8..2fd28c4b6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -98,19 +98,35 @@ protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added, int depth) { - for (JSMethodInfo method : type.getMethods().values()) { - String name = method.getName(); - // Skip overload markers (name$1, name$2, etc.) - if (name.contains("$")) { - name = name.substring(0, name.indexOf('$')); + // Collect base method names (without $N suffix) that we haven't processed yet + Set baseNames = new HashSet<>(); + for (String key : type.getMethods().keySet()) { + String baseName = key.contains("$") ? key.substring(0, key.indexOf('$')) : key; + if (!added.contains(baseName)) { + baseNames.add(baseName); } - if (!added.contains(name)) { - added.add(name); - // Pass contextType (original receiver) for type parameter resolution, not current type + } + + // For each base method name, add all overloads + for (String baseName : baseNames) { + added.add(baseName); + // Use getMethodOverloads to get all overloads for this method (excluding inherited) + List overloads = new ArrayList<>(); + if (type.getMethods().containsKey(baseName)) { + overloads.add(type.getMethods().get(baseName)); + } + int index = 1; + while (type.getMethods().containsKey(baseName + "$" + index)) { + overloads.add(type.getMethods().get(baseName + "$" + index)); + index++; + } + // Add one autocomplete item per overload + for (JSMethodInfo method : overloads) { items.add(AutocompleteItem.fromJSMethod(method, contextType, depth)); } } @@ -161,7 +177,7 @@ protected void addLanguageUniqueSuggestions(Context context, List globalNames = new ArrayList<>(); + List globalNames = new ArrayList<>(); globalNames.addAll(registry.getGlobalEngineObjects().keySet()); globalNames.addAll(document.getEditorGlobals().keySet()); From 5795cc984b910bf99f029efe591bffbac9ef07f5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 27 Jan 2026 17:10:31 +0200 Subject: [PATCH 311/337] Completely parsed type generics. List list; list.get(0) NOW returns IPlayer, instead of T or Object. Nested generics are also handled and functional: Map> Implemented TokenHoverInfo/Autocomplete rendering and handling for generics as well. --- .../script/autocomplete/AutocompleteItem.java | 22 +- .../script/interpreter/ScriptDocument.java | 146 ++++-- .../script/interpreter/field/FieldInfo.java | 14 +- .../interpreter/hover/TokenHoverInfo.java | 82 +++- .../interpreter/js_parser/JSFieldInfo.java | 5 +- .../interpreter/js_parser/JSMethodInfo.java | 71 +-- .../interpreter/js_parser/TypeParamInfo.java | 6 +- .../js_parser/TypeScriptDefinitionParser.java | 60 ++- .../interpreter/method/MethodCallInfo.java | 4 +- .../script/interpreter/method/MethodInfo.java | 43 +- .../interpreter/type/GenericTypeParser.java | 432 ++++++++++++++++++ .../script/interpreter/type/TypeInfo.java | 257 ++++++++++- .../script/interpreter/type/TypeResolver.java | 90 ++-- .../interpreter/type/TypeSubstitutor.java | 222 +++++++++ 14 files changed, 1303 insertions(+), 151 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index cf1201769..fdbf3cf86 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -239,16 +239,16 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contex if (i > 0) displayName.append(", "); JSMethodInfo.JSParameterInfo param = method.getParameters().get(i); - // Use resolved type + // Use resolved type with full generic display TypeInfo paramType = param.getResolvedType(contextType); - String paramTypeName = paramType.isResolved() ? paramType.getSimpleName() : param.getType(); + String paramTypeName = paramType.isResolved() ? paramType.getDisplayName() : param.getType(); displayName.append(paramTypeName).append(" ").append(param.getName()); } displayName.append(")"); - // Get return type using the new method + // Get return type using the new method with full generic display TypeInfo returnTypeInfo = method.getResolvedReturnType(contextType); - String returnType = returnTypeInfo.isResolved() ? returnTypeInfo.getSimpleName() : method.getReturnType(); + String returnType = returnTypeInfo.isResolved() ? returnTypeInfo.getDisplayName() : method.getReturnType(); return new AutocompleteItem( displayName.toString(), @@ -290,9 +290,9 @@ public static AutocompleteItem fromJSField(JSFieldInfo field, int inheritanceDep * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) */ public static AutocompleteItem fromJSField(JSFieldInfo field, TypeInfo contextType, int inheritanceDepth) { - // Get field type using the new method + // Get field type using the new method with full generic display TypeInfo fieldTypeInfo = field.getResolvedType(contextType); - String fieldType = fieldTypeInfo.isResolved() ? fieldTypeInfo.getSimpleName() : field.getType(); + String fieldType = fieldTypeInfo.isResolved() ? fieldTypeInfo.getDisplayName() : field.getType(); return new AutocompleteItem( field.getName(), @@ -549,8 +549,16 @@ public void addScoreBoost(int boost) { public int getMatchScore() { return matchScore; } public int[] getMatchIndices() { return matchIndices; } + /** + * Get the display name for a type, including generic arguments. + * Uses the unified getDisplayName() method which handles: + * - Simple types: "String" + * - Parameterized types: "List", "Map" + * - Nested generics: "List>" + * - JS types with namespace: "IPlayerEvent.InteractEvent" + */ public static String getName(TypeInfo type) { - return type.isJSType() ? type.getFullName() : type.getSimpleName(); + return type.getDisplayName(); } public int getParameterCount() { 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 a571f9576..516f68d57 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 @@ -1916,52 +1916,130 @@ private TypeInfo resolveTypeAndTrackUsage(String typeName) { if (typeName == null || typeName.isEmpty()) return TypeInfo.unresolved(typeName, typeName); - // Strip generics for resolution - int genericStart = typeName.indexOf('<'); - String baseName = genericStart > 0 ? typeName.substring(0, genericStart) : typeName; + String normalized = typeName.trim(); + normalized = stripLeadingModifiers(normalized); - // Strip array brackets - baseName = baseName.replace("[]", "").trim(); + // Preserve and resolve nested generics (e.g., List>) + GenericTypeParser.ParsedType parsed = GenericTypeParser.parse(normalized); + if (parsed == null) { + return TypeInfo.unresolved(normalized, normalized); + } - // Check primitive types first - if (TypeResolver.isPrimitiveType(baseName)) { - return TypeInfo.fromPrimitive(baseName); + TypeInfo result = resolveJavaFromParsedType(parsed); + return result != null ? result : TypeInfo.unresolved(parsed.baseName, normalized); + } + + /** + * Strip only LEADING Java modifiers from a type expression. + * This avoids breaking generic argument lists which may contain whitespace. + */ + private String stripLeadingModifiers(String typeExpr) { + if (typeExpr == null) { + return null; } + int i = 0; + int len = typeExpr.length(); + while (i < len) { + while (i < len && Character.isWhitespace(typeExpr.charAt(i))) i++; + int start = i; + while (i < len && Character.isJavaIdentifierPart(typeExpr.charAt(i))) i++; + if (start == i) { + break; + } + String word = typeExpr.substring(start, i); + if (TypeResolver.isModifier(word)) { + // Continue stripping modifiers + continue; + } + // Not a modifier; rewind to the start of this token + return typeExpr.substring(start).trim(); + } + return typeExpr.trim(); + } - // Check for String (common case) - if (baseName.equals("String")) { - return typeResolver.resolveFullName("java.lang.String"); + /** + * Resolve a GenericTypeParser ParsedType for Java/Groovy scripts, preserving type arguments. + * Also tracks import usage for base types and nested type arguments. + */ + private TypeInfo resolveJavaFromParsedType(GenericTypeParser.ParsedType parsed) { + if (parsed == null) { + return null; } - // Check script-defined types FIRST - if (scriptTypes.containsKey(baseName)) { - return scriptTypes.get(baseName); + String baseName = parsed.baseName; + if (baseName == null || baseName.isEmpty()) { + return null; } - // Check if there's an explicit import for this type - ImportData usedImport = importsBySimpleName.get(baseName); - - // Try to resolve through imports/classpath - TypeInfo result = typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); - - // Track the import usage if we found one and the resolution succeeded - if (result != null && usedImport != null) { - usedImport.incrementUsage(); + // Normalize whitespace around dots + baseName = baseName.replaceAll("\\s*\\.\\s*", ".").trim(); + + TypeInfo baseType; + + // Primitives + if (TypeResolver.isPrimitiveType(baseName)) { + baseType = TypeInfo.fromPrimitive(baseName); } - - // Also check wildcard imports if the type was resolved through one - if (result != null && usedImport == null && wildcardPackages != null) { - String resultPkg = result.getPackageName(); - // Check if resolved through a wildcard - for (ImportData imp : imports) { - if (imp.isWildcard() && resultPkg != null && resultPkg.equals(imp.getFullPath())) { - imp.incrementUsage(); - break; + // Common java.lang.String + else if ("String".equals(baseName)) { + baseType = typeResolver.resolveFullName("java.lang.String"); + } + // Script-defined types (simple names only) + else if (!baseName.contains(".") && scriptTypes.containsKey(baseName)) { + baseType = scriptTypes.get(baseName); + } + // Fully-qualified + else if (baseName.contains(".")) { + baseType = typeResolver.resolveFullName(baseName); + } + // Imported/simple + else { + baseType = typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + + // Track explicit import usage if present + ImportData usedImport = importsBySimpleName.get(baseName); + if (baseType != null && baseType.isResolved() && usedImport != null) { + usedImport.incrementUsage(); + } + + // Track wildcard import usage if resolved through a wildcard + if (baseType != null && baseType.isResolved() && usedImport == null && wildcardPackages != null) { + String resultPkg = baseType.getPackageName(); + for (ImportData imp : imports) { + if (imp.isWildcard() && resultPkg != null && resultPkg.equals(imp.getFullPath())) { + imp.incrementUsage(); + break; + } } } } - - return result; + + if (baseType == null) { + baseType = TypeInfo.unresolved(baseName, baseName); + } + + // Apply generic arguments + if (parsed.hasTypeArgs() && baseType.isResolved()) { + java.util.List resolvedArgs = new java.util.ArrayList<>(); + for (GenericTypeParser.ParsedType argParsed : parsed.typeArgs) { + TypeInfo argType = resolveJavaFromParsedType(argParsed); + resolvedArgs.add(argType != null ? argType : TypeInfo.unresolved(argParsed.baseName, argParsed.baseName)); + } + if (!resolvedArgs.isEmpty()) { + baseType = baseType.parameterize(resolvedArgs); + } + } + + // Apply array dimensions + if (parsed.isArray) { + TypeInfo arrayType = baseType; + for (int i = 0; i < parsed.arrayDimensions; i++) { + arrayType = TypeInfo.arrayOf(arrayType); + } + baseType = arrayType; + } + + return baseType; } // ==================== PHASE 4: BUILD MARKS ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index bbf74f644..e4239074a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -7,6 +7,7 @@ import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; @@ -146,10 +147,21 @@ public static FieldInfo external(String name, TypeInfo type, String documentatio /** * Create a FieldInfo from reflection data for a class field. + * Preserves generic type information like List. */ public static FieldInfo fromReflection(Field field, TypeInfo containingType) { String name = field.getName(); - TypeInfo type = TypeInfo.fromClass(field.getType()); + // Use getGenericType() to preserve generic information + TypeInfo type = TypeInfo.fromGenericType(field.getGenericType()); + if (type == null) { + type = TypeInfo.fromClass(field.getType()); + } + + // If the receiver is parameterized, substitute class type variables in the field type + java.util.Map receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); + if (!receiverBindings.isEmpty()) { + type = TypeSubstitutor.substitute(type, receiverBindings); + } // Check if this is an enum constant if (field.isEnumConstant()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 8520992f8..8c0669dbe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -545,7 +545,8 @@ private void extractClassInfo(Token token) { int classColor = clazz.isInterface() ? TokenType.INTERFACE_DECL.getHexColor() : clazz.isEnum() ? TokenType.ENUM_DECL.getHexColor() : TokenType.IMPORTED_CLASS.getHexColor(); - addSegment(getName(typeInfo), classColor); + // Render generics with correct segment colors if present + addTypeSegments(typeInfo); // Extends Class superclass = clazz.getSuperclass(); @@ -604,7 +605,7 @@ private void extractClassInfo(Token token) { int classColor = scriptType.getKind() == TypeInfo.Kind.INTERFACE ? TokenType.INTERFACE_DECL.getHexColor() : scriptType.getKind() == TypeInfo.Kind.ENUM ? TokenType.ENUM_DECL.getHexColor() : TokenType.IMPORTED_CLASS.getHexColor(); - addSegment(getName(typeInfo), classColor); + addTypeSegments(typeInfo); // Extends clause for ScriptTypeInfo if (scriptType.hasSuperClass()) { @@ -612,7 +613,7 @@ private void extractClassInfo(Token token) { TypeInfo superClass = scriptType.getSuperClass(); if (superClass != null && superClass.isResolved()) { // Color based on resolved type - addSegment(getName(superClass), getColorForTypeInfo(superClass)); + addTypeSegments(superClass); } else { // Unresolved - show the raw name in undefined color String superName = scriptType.getSuperClassName(); @@ -642,7 +643,8 @@ private void extractClassInfo(Token token) { String ifaceName = (i < interfaceNames.size()) ? interfaceNames.get(i) : getName(ifaceType); if (ifaceType != null && ifaceType.isResolved()) { - addSegment(ifaceName, getColorForTypeInfo(ifaceType)); + // Prefer rendering from the resolved TypeInfo so generics color correctly + addTypeSegments(ifaceType); } else { addSegment(ifaceName, TokenType.UNDEFINED_VAR.getHexColor()); } @@ -675,7 +677,7 @@ private void extractClassInfo(Token token) { iconIndicator = "I"; addSegment("interface ", TokenType.MODIFIER.getHexColor()); - addSegment(getName(typeInfo), TokenType.INTERFACE_DECL.getHexColor()); + addTypeSegments(typeInfo); if (jsType.getExtendsType() != null) { addSegment(" extends ", TokenType.MODIFIER.getHexColor()); @@ -831,8 +833,7 @@ private void extractGlobalFieldInfo(Token token) { packageName = pkg; // Type - check for actual type color - int typeColor = getColorForTypeInfo(declaredType); - addSegment(getName(declaredType), typeColor); + addTypeSegments(declaredType); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -872,8 +873,7 @@ private void extractEnumConstantInfo(Token token) { // Type (enum type name) - int typeColor = getColorForTypeInfo(enumType); - addSegment(getName(enumType), typeColor); + addTypeSegments(enumType); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -900,8 +900,7 @@ private void extractLocalFieldInfo(Token token) { TypeInfo declaredType = fieldInfo.getTypeInfo(); if (declaredType != null) { - int typeColor = getColorForTypeInfo(declaredType); - addSegment(getName(declaredType), typeColor); + addTypeSegments(declaredType); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -948,8 +947,7 @@ private void extractParameterInfo(Token token) { TypeInfo declaredType = fieldInfo.getTypeInfo(); if (declaredType != null) { - int typeColor = getColorForTypeInfo(declaredType); - addSegment(getName(declaredType), typeColor); + addTypeSegments(declaredType); addSegment(" ", TokenType.DEFAULT.getHexColor()); } @@ -963,8 +961,55 @@ private void extractUndefinedInfo(Token token) { //addSegment(token.getText(), TokenType.UNDEFINED_VAR.getHexColor()); } + /** + * Get the display name for a type, including generic arguments. + * Uses the unified getDisplayName() method which handles: + * - Simple types: "String" + * - Parameterized types: "List", "Map" + * - Nested generics: "List>" + * - JS types with namespace: "IPlayerEvent.InteractEvent" + */ public String getName(TypeInfo type){ - return type.isJSType()? type.getFullName() : type.getSimpleName(); + return type.getDisplayName(); + } + + /** + * Add a type name as multiple colored segments so generics render like the editor marks. + * Example: List -> "List" (interface), "<" (default), "String" (class), ">" (default) + */ + private void addTypeSegments(TypeInfo typeInfo) { + addTypeSegments(typeInfo, 0); + } + + private void addTypeSegments(TypeInfo typeInfo, int depth) { + if (typeInfo == null) { + addSegment("any", TokenType.DEFAULT.getHexColor()); + return; + } + if (depth > 25) { + // Safety guard against pathological recursive types + addSegment(getName(typeInfo), getColorForTypeInfo(typeInfo)); + return; + } + + if (typeInfo.isParameterized()) { + TypeInfo raw = typeInfo.getRawType(); + addSegment(getName(raw), getColorForTypeInfo(raw)); + addSegment("<", TokenType.DEFAULT.getHexColor()); + + java.util.List args = typeInfo.getAppliedTypeArgs(); + for (int i = 0; i < args.size(); i++) { + if (i > 0) { + addSegment(", ", TokenType.DEFAULT.getHexColor()); + } + addTypeSegments(args.get(i), depth + 1); + } + + addSegment(">", TokenType.DEFAULT.getHexColor()); + return; + } + + addSegment(getName(typeInfo), getColorForTypeInfo(typeInfo)); } private void extractFieldInfoGeneric(Token token) { @@ -1003,8 +1048,7 @@ private void buildConstructorDeclaration(MethodInfo constructor, TypeInfo contai FieldInfo param = params.get(i); TypeInfo paramType = param.getTypeInfo(); if (paramType != null) { - int paramTypeColor = getColorForTypeInfo(paramType); - addSegment(getName(paramType), paramTypeColor); + addTypeSegments(paramType); addSegment(" ", TokenType.DEFAULT.getHexColor()); } addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); @@ -1082,8 +1126,7 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain // Return type TypeInfo returnType = methodInfo.getReturnType(); if (returnType != null) { - int returnTypeColor = getColorForTypeInfo(returnType); - addSegment(getName(returnType), returnTypeColor); + addTypeSegments(returnType); addSegment(" ", TokenType.DEFAULT.getHexColor()); } else { addSegment("void ", TokenType.KEYWORD.getHexColor()); @@ -1100,8 +1143,7 @@ private void buildBasicMethodDeclaration(MethodInfo methodInfo, TypeInfo contain FieldInfo param = params.get(i); TypeInfo paramType = param.getTypeInfo(); if (paramType != null) { - int paramTypeColor = getColorForTypeInfo(paramType); - addSegment(getName(paramType), paramTypeColor); + addTypeSegments(paramType); addSegment(" ", TokenType.DEFAULT.getHexColor()); } addSegment(param.getName(), TokenType.PARAMETER.getHexColor()); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java index afcb09a6b..c09a1e2b8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java @@ -79,11 +79,12 @@ public String getDocumentation() { } /** - * Get display name - uses resolved TypeInfo simple name if available. + * Get display name - uses resolved TypeInfo display name if available, + * including generic type arguments like List. */ public String getDisplayType() { if (typeInfo != null && typeInfo.isResolved()) { - return typeInfo.getSimpleName(); + return typeInfo.getDisplayName(); } return type; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java index 6aef02b7f..e8c1d7f2b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java @@ -3,6 +3,7 @@ import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; import java.util.*; @@ -47,24 +48,31 @@ public void setContainingType(JSTypeInfo containingType) { } /** - * Get the resolved return type, with fallback resolution using type parameters. - * @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP) - * @return The resolved TypeInfo for the return type + * Get the resolved return type, with type parameter substitution based on receiver context. + * + * For parameterized receivers like List, this will substitute type variables: + * - If return type is "T" and receiver is List, returns TypeInfo(String) + * - Handles nested generics like List> with proper substitution + * + * @param contextType The TypeInfo context for resolving type parameters (e.g., List to resolve E → String) + * @return The resolved TypeInfo for the return type, with type variables substituted */ public TypeInfo getResolvedReturnType(TypeInfo contextType) { - // Use pre-resolved if available - if (returnTypeInfo != null && returnTypeInfo.isResolved()) - return returnTypeInfo; - - // Fall back to resolving from string TypeResolver resolver = TypeResolver.getInstance(); - TypeInfo resolved = returnTypeInfo = resolver.resolveJSType(returnType); - - // If not resolved and contextType has type parameters, try to resolve as type parameter - if (contextType != null && !resolved.isResolved()) { + + // First, resolve the return type string to a TypeInfo (now preserves generic args) + TypeInfo resolved = resolver.resolveJSType(returnType); + + // If we have a parameterized context, apply type variable substitution + if (contextType != null && contextType.isParameterized()) { + resolved = TypeSubstitutor.getSubstitutedReturnType(resolved, returnType, contextType, resolver); + } + // Fallback: if not resolved and contextType has declared type parameters, try direct resolution + else if (contextType != null && !resolved.isResolved()) { TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(returnType); - if (paramResolution != null) - resolved = returnTypeInfo = paramResolution; //cache it to returnTypeInfo + if (paramResolution != null) { + resolved = paramResolution; + } } return resolved; @@ -150,24 +158,30 @@ public void setTypeInfo(TypeInfo typeInfo) { } /** - * Get the resolved type, with fallback resolution using type parameters. + * Get the resolved type, with type parameter substitution based on receiver context. + * + * For parameterized receivers like Consumer, this will substitute type variables: + * - If param type is "T" and receiver is Consumer, returns TypeInfo(IAction) + * * @param contextType The TypeInfo context for resolving type parameters * @return The resolved TypeInfo for the parameter type */ public TypeInfo getResolvedType(TypeInfo contextType) { - // Use pre-resolved if available - if (typeInfo != null && typeInfo.isResolved()) - return typeInfo; - - // Fall back to resolving from string TypeResolver resolver = TypeResolver.getInstance(); - TypeInfo resolved = typeInfo = resolver.resolveJSType(type); - - // If not resolved and contextType has type parameters, try to resolve as type parameter - if (contextType != null && !resolved.isResolved()) { + + // First, resolve the type string to a TypeInfo (now preserves generic args) + TypeInfo resolved = resolver.resolveJSType(type); + + // If we have a parameterized context, apply type variable substitution + if (contextType != null && contextType.isParameterized()) { + resolved = TypeSubstitutor.getSubstitutedReturnType(resolved, type, contextType, resolver); + } + // Fallback: if not resolved and contextType has declared type parameters, try direct resolution + else if (contextType != null && !resolved.isResolved()) { TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(type); - if (paramResolution != null) - resolved = typeInfo = paramResolution; //cache it to typeInfo + if (paramResolution != null) { + resolved = paramResolution; + } } return resolved; @@ -179,11 +193,12 @@ public TypeInfo getResolvedType(TypeInfo contextType) { public JSMethodInfo getContainingMethod() {return containingMethod;} /** - * Get display name - uses resolved TypeInfo simple name if available. + * Get display name - uses resolved TypeInfo display name if available, + * including generic type arguments like List. */ public String getDisplayType() { if (typeInfo != null && typeInfo.isResolved()) { - return typeInfo.getSimpleName(); + return typeInfo.getDisplayName(); } return type; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java index d41005a6e..846a23f47 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/TypeParamInfo.java @@ -48,18 +48,18 @@ public void resolveBoundType() { } /** - * Get simple display name for the bound type. + * Get display name for the bound type, including generic arguments. */ public String getBoundTypeName() { if (boundTypeInfo == null) return null; - return boundTypeInfo.getSimpleName(); + return boundTypeInfo.getDisplayName(); } @Override public String toString() { if (boundTypeInfo != null) { - return name + " extends " + boundTypeInfo.getSimpleName(); + return name + " extends " + boundTypeInfo.getDisplayName(); } else if (boundType != null || fullBoundType != null) { return name + " extends " + (boundType != null ? boundType : fullBoundType); } 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 9d8bc367c..b4c26f69c 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 @@ -40,13 +40,13 @@ public class TypeScriptDefinitionParser { "export\\s+type\\s+(\\w+)\\s*=\\s*([^;\\n]+);?"); private static final Pattern METHOD_PATTERN = Pattern.compile( - "^\\s*(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*([^;]+);", Pattern.MULTILINE); + "(?m)^\\s*(\\w+)\\s*\\((.*)\\)\\s*:\\s*([^;]+);", Pattern.MULTILINE); private static final Pattern FIELD_PATTERN = Pattern.compile( "^\\s*(readonly\\s+)?(\\w+)\\s*:\\s*([^;]+);", Pattern.MULTILINE); private static final Pattern GLOBAL_FUNCTION_PATTERN = Pattern.compile( - "function\\s+(\\w+)\\s*\\(([^)]*)\\)\\s*:\\s*([^;]+);"); + "function\\s+(\\w+)\\s*\\((.*)\\)\\s*:\\s*([^;]+);"); private static final Pattern GLOBAL_TYPE_ALIAS_PATTERN = Pattern.compile( "type\\s+(\\w+)\\s*=\\s*import\\(['\"]([^'\"]+)['\"]\\)\\.([\\w.]+);"); @@ -666,19 +666,53 @@ private List splitParameters(String params) { private String cleanType(String type) { if (type == null) return "any"; type = type.trim(); - - // Handle import('./path').TypeName syntax - if (type.startsWith("import(")) { - // Extract the type name after the import - int dotIndex = type.lastIndexOf(")."); - if (dotIndex > 0) { - type = type.substring(dotIndex + 2); + + // Strip TypeScript import() type references anywhere in the string. + // Examples: + // - import('./data/IAction').IAction -> IAction + // - Java.java.util.function.Consumer -> Java.java.util.function.Consumer + // This is important because import() can appear nested inside generics. + int importIdx; + while ((importIdx = type.indexOf("import(")) >= 0) { + int i = importIdx + "import(".length(); + int depth = 1; + boolean inString = false; + char stringChar = 0; + + while (i < type.length() && depth > 0) { + char c = type.charAt(i); + if (inString) { + if (c == stringChar && (i == 0 || type.charAt(i - 1) != '\\')) { + inString = false; + } + } else { + if (c == '\'' || c == '"') { + inString = true; + stringChar = c; + } else if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } + } + i++; + } + + // i is positioned just after the matching ')', if found. + if (depth != 0) { + break; // Unbalanced import( ... ) - stop trying to clean. + } + + // If immediately followed by ".", remove "import(...) ." prefix. + if (i < type.length() && type.charAt(i) == '.') { + int removeEnd = i + 1; + type = type.substring(0, importIdx) + type.substring(removeEnd); + } else { + // Remove just the import(...) portion. + type = type.substring(0, importIdx) + type.substring(i); } } - - // Handle arrays - type = type.replace("[]", "[]"); - + return type; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index c6362d701..ec396b7bb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -388,8 +388,8 @@ public void validateArgTypeError() { TypeInfo paramType = para.getTypeInfo(); if (argType != null && paramType != null) { if (!TypeChecker.isTypeCompatible(paramType, argType)) { - setArgTypeError(i, "Expected " + paramType.getSimpleName() + - " but got " + argType.getSimpleName()); + setArgTypeError(i, "Expected " + paramType.getDisplayName() + + " but got " + argType.getDisplayName()); } } else if (paramType == null) { setArgTypeError(i, "Parameter type of '" + para.getName() + "' is unresolved"); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 325ca4b56..1315ff72a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -10,6 +10,7 @@ import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; import java.lang.reflect.Constructor; import java.lang.reflect.Method; @@ -140,10 +141,15 @@ public static MethodInfo external(String name, TypeInfo returnType, TypeInfo con /** * Create a MethodInfo from reflection data. * Used when resolving method calls on known types. + * Preserves generic type information from reflection. */ public static MethodInfo fromReflection(Method method, TypeInfo containingType) { String name = method.getName(); - TypeInfo returnType = TypeInfo.fromClass(method.getReturnType()); + // Use getGenericReturnType() to preserve generic information like List + TypeInfo returnType = TypeInfo.fromGenericType(method.getGenericReturnType()); + if (returnType == null) { + returnType = TypeInfo.fromClass(method.getReturnType()); + } int modifiers = method.getModifiers(); // Try to find matching JSMethodInfo to bridge parameter names/docs and allow return overrides @@ -166,11 +172,25 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) } } } + + // If the receiver is parameterized (e.g., List), substitute class type variables (E -> String) + java.util.Map receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); + if (!receiverBindings.isEmpty()) { + returnType = TypeSubstitutor.substitute(returnType, receiverBindings); + } List params = new ArrayList<>(); - Class[] paramTypes = method.getParameterTypes(); + // Use getGenericParameterTypes() to preserve generic information + java.lang.reflect.Type[] genericParamTypes = method.getGenericParameterTypes(); List jsParams = jsMethod != null ? jsMethod.getParameters() : null; - for (int i = 0; i < paramTypes.length; i++) { - TypeInfo paramType = TypeInfo.fromClass(paramTypes[i]); + for (int i = 0; i < genericParamTypes.length; i++) { + TypeInfo paramType = TypeInfo.fromGenericType(genericParamTypes[i]); + if (paramType == null) { + paramType = TypeInfo.fromClass(method.getParameterTypes()[i]); + } + + if (!receiverBindings.isEmpty()) { + paramType = TypeSubstitutor.substitute(paramType, receiverBindings); + } String paramName = "arg" + i; if (jsParams != null && i < jsParams.size()) { String jsName = jsParams.get(i).getName(); @@ -199,6 +219,7 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) /** * Create a MethodInfo from a Constructor via reflection. * Used when resolving constructor calls on external types. + * Preserves generic type information for constructor parameters. */ public static MethodInfo fromReflectionConstructor(Constructor constructor, TypeInfo containingType) { String name = containingType.getSimpleName(); // Constructor name is the type name @@ -206,9 +227,13 @@ public static MethodInfo fromReflectionConstructor(Constructor constructor, T int modifiers = constructor.getModifiers(); List params = new ArrayList<>(); - Class[] paramTypes = constructor.getParameterTypes(); - for (int i = 0; i < paramTypes.length; i++) { - TypeInfo paramType = TypeInfo.fromClass(paramTypes[i]); + // Use getGenericParameterTypes() to preserve generic information + java.lang.reflect.Type[] genericParamTypes = constructor.getGenericParameterTypes(); + for (int i = 0; i < genericParamTypes.length; i++) { + TypeInfo paramType = TypeInfo.fromGenericType(genericParamTypes[i]); + if (paramType == null) { + paramType = TypeInfo.fromClass(constructor.getParameterTypes()[i]); + } params.add(FieldInfo.reflectionParam("arg" + i, paramType)); } @@ -605,7 +630,7 @@ private void validateParameters() { // Check for unresolved parameter types TypeInfo paramType = param.getTypeInfo(); if (paramType == null || !paramType.isResolved()) { - String typeName = paramType != null ? paramType.getSimpleName() : "unknown"; + String typeName = paramType != null ? paramType.getDisplayName() : "unknown"; addParameterError(param, i, ErrorType.PARAMETER_UNDEFINED, "Cannot resolve parameter type '" + typeName + "'"); } @@ -639,7 +664,7 @@ private void validateReturnTypes(String bodyText, TypeResolver typeResolver) { if (bodyText == null || bodyText.isEmpty()) return; boolean isVoid = TypeChecker.isVoidType(returnType); - String expectedTypeName = isVoid ? "void" : returnType.getSimpleName(); + String expectedTypeName = isVoid ? "void" : returnType.getDisplayName(); // Remove comments but keep strings (we need accurate positions) String cleanBody = CodeParser.removeComments(bodyText); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java new file mode 100644 index 000000000..68a244179 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java @@ -0,0 +1,432 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import java.util.ArrayList; +import java.util.List; + +/** + * Parser for generic type expressions with nested type arguments. + * + * Handles type expressions like: + * - Simple types: "String", "IPlayer" + * - Qualified names: "java.util.List", "IPlayerEvent.InteractEvent" + * - Single generic: "List", "Consumer" + * - Multiple generics: "Map" + * - Nested generics: "List>" + * - Arrays: "String[]", "List[]" + * - Nullable: "String?" + * - Union types: "String | null" (uses first type for resolution) + * + * This parser produces a ParsedType tree that can be resolved into parameterized TypeInfo instances. + */ +public class GenericTypeParser { + + /** + * Represents a parsed type expression with potential generic arguments. + */ + public static class ParsedType { + /** The base type name (may be qualified, e.g., "java.util.List" or "IPlayer") */ + public final String baseName; + + /** The applied type arguments (for generics like List), each is itself a ParsedType */ + public final List typeArgs; + + /** Whether this type is an array (has [] suffix) */ + public final boolean isArray; + + /** Array dimensions (1 for T[], 2 for T[][], etc.) */ + public final int arrayDimensions; + + /** Whether this type is nullable (has ? suffix in TypeScript) */ + public final boolean isNullable; + + /** The original raw string before parsing */ + public final String rawString; + + public ParsedType(String baseName, List typeArgs, boolean isArray, + int arrayDimensions, boolean isNullable, String rawString) { + this.baseName = baseName; + this.typeArgs = typeArgs != null ? typeArgs : new ArrayList<>(); + this.isArray = isArray; + this.arrayDimensions = arrayDimensions; + this.isNullable = isNullable; + this.rawString = rawString; + } + + /** + * Check if this parsed type has generic arguments. + */ + public boolean hasTypeArgs() { + return !typeArgs.isEmpty(); + } + + /** + * Get display string for debugging. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(baseName); + if (!typeArgs.isEmpty()) { + sb.append("<"); + for (int i = 0; i < typeArgs.size(); i++) { + if (i > 0) sb.append(", "); + sb.append(typeArgs.get(i).toString()); + } + sb.append(">"); + } + for (int i = 0; i < arrayDimensions; i++) { + sb.append("[]"); + } + if (isNullable) { + sb.append("?"); + } + return sb.toString(); + } + } + + /** + * Parse a type expression string into a ParsedType tree. + * + * @param typeExpr The type expression to parse (e.g., "List>[]") + * @return The parsed type, or null if parsing fails + */ + public static ParsedType parse(String typeExpr) { + if (typeExpr == null || typeExpr.isEmpty()) { + return null; + } + + String expr = typeExpr.trim(); + + // Handle union types - use first type (same as current behavior) + if (expr.contains("|")) { + String[] parts = expr.split("\\|"); + expr = parts[0].trim(); + } + + // Handle nullable marker (TypeScript ?), remove it but track + boolean isNullable = false; + if (expr.endsWith("?")) { + isNullable = true; + expr = expr.substring(0, expr.length() - 1).trim(); + } + + // Count and strip array dimensions + int arrayDims = 0; + while (expr.endsWith("[]")) { + arrayDims++; + expr = expr.substring(0, expr.length() - 2).trim(); + } + + // Now parse the base type with potential generic arguments + ParseResult result = parseTypeWithGenerics(expr, 0); + if (result == null || result.type == null) { + // Fallback: treat entire expression as a simple type + return new ParsedType(expr, null, arrayDims > 0, arrayDims, isNullable, typeExpr); + } + + ParsedType parsed = result.type; + + // Apply array and nullable modifiers + return new ParsedType(parsed.baseName, parsed.typeArgs, + arrayDims > 0, arrayDims, isNullable, typeExpr); + } + + /** + * Internal result class for recursive parsing. + */ + private static class ParseResult { + final ParsedType type; + final int endIndex; // position after this type in the string + + ParseResult(ParsedType type, int endIndex) { + this.type = type; + this.endIndex = endIndex; + } + } + + /** + * Parse a type expression starting at the given index, handling nested generics. + * Returns the parsed type and the index after this type expression. + */ + private static ParseResult parseTypeWithGenerics(String expr, int startIndex) { + int i = startIndex; + int len = expr.length(); + + // Skip leading whitespace + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + if (i >= len) { + return null; + } + + // Special-case: TypeScript import() type references like import('./x').TypeName + // We normalize these to just the referenced type name (e.g., "TypeName" or "Ns.TypeName"). + String baseName; + if (expr.startsWith("import(", i)) { + int j = i + "import(".length(); + int depth = 1; + boolean inString = false; + char stringChar = 0; + while (j < len && depth > 0) { + char c = expr.charAt(j); + if (inString) { + if (c == stringChar && (j == 0 || expr.charAt(j - 1) != '\\')) { + inString = false; + } + } else { + if (c == '\'' || c == '"') { + inString = true; + stringChar = c; + } else if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } + } + j++; + } + // j is positioned just after ')' + i = j; + // Skip whitespace + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + // Optional dot before the referenced type + if (i < len && expr.charAt(i) == '.') { + i++; + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + } + + StringBuilder nameBuilder = new StringBuilder(); + while (i < len) { + char c = expr.charAt(i); + if (Character.isJavaIdentifierPart(c) || c == '.') { + nameBuilder.append(c); + i++; + } else { + break; + } + } + baseName = nameBuilder.toString().trim(); + } else { + // Read the base type name (can include dots for qualified names) + StringBuilder baseNameBuilder = new StringBuilder(); + while (i < len) { + char c = expr.charAt(i); + if (Character.isJavaIdentifierPart(c) || c == '.') { + baseNameBuilder.append(c); + i++; + } else { + break; + } + } + baseName = baseNameBuilder.toString().trim(); + } + + if (baseName.isEmpty()) { + return null; + } + + // Remove trailing dots if any + while (baseName.endsWith(".")) { + baseName = baseName.substring(0, baseName.length() - 1); + } + + // Skip whitespace before potential < + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + // Check for generic arguments + List typeArgs = new ArrayList<>(); + if (i < len && expr.charAt(i) == '<') { + i++; // Skip '<' + + // Parse type arguments (comma-separated, handling nested <>) + while (i < len) { + // Skip whitespace + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + if (i >= len) break; + + char c = expr.charAt(i); + if (c == '>') { + i++; // Skip '>' + break; + } + + if (c == ',') { + i++; // Skip ',' + continue; + } + + // Parse a type argument (may itself have generics) + ParseResult argResult = parseTypeArgument(expr, i); + if (argResult != null && argResult.type != null) { + typeArgs.add(argResult.type); + i = argResult.endIndex; + } else { + // Skip forward until the next ',' or '>' at the current nesting level. + i = skipToNextTypeArgDelimiter(expr, i); + } + } + } + + ParsedType type = new ParsedType(baseName, typeArgs, false, 0, false, expr.substring(startIndex, i)); + return new ParseResult(type, i); + } + + /** + * Skip forward until we hit ',' or '>' that would delimit a type argument. + * This prevents producing spurious additional type args when encountering unsupported syntax. + */ + private static int skipToNextTypeArgDelimiter(String expr, int startIndex) { + int i = startIndex; + int depth = 0; + boolean inString = false; + char stringChar = 0; + while (i < expr.length()) { + char c = expr.charAt(i); + if (inString) { + if (c == stringChar && (i == 0 || expr.charAt(i - 1) != '\\')) { + inString = false; + } + i++; + continue; + } + + if (c == '\'' || c == '"') { + inString = true; + stringChar = c; + i++; + continue; + } + + if (c == '<' || c == '(' || c == '[') { + depth++; + } else if (c == '>' || c == ')' || c == ']') { + if (depth > 0) { + depth--; + } else if (c == '>') { + return i; // Let caller handle closing '>' + } + } else if ((c == ',' || c == '>') && depth == 0) { + return i; + } + i++; + } + return i; + } + + /** + * Parse a single type argument, which may include nested generics and array suffixes. + */ + private static ParseResult parseTypeArgument(String expr, int startIndex) { + int i = startIndex; + int len = expr.length(); + + // Skip leading whitespace + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + if (i >= len) { + return null; + } + + // Check for wildcard (? extends X, ? super X, or just ?) + if (expr.charAt(i) == '?') { + i++; + // Skip whitespace + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + // Check for "extends" or "super" + if (i < len && expr.substring(i).startsWith("extends ")) { + i += 8; // Skip "extends " + // Parse the bound type + ParseResult boundResult = parseTypeWithGenerics(expr, i); + if (boundResult != null) { + return boundResult; + } + } else if (i < len && expr.substring(i).startsWith("super ")) { + i += 6; // Skip "super " + // Parse the bound type + ParseResult boundResult = parseTypeWithGenerics(expr, i); + if (boundResult != null) { + return boundResult; + } + } + // Plain wildcard, treat as Object + return new ParseResult(new ParsedType("Object", null, false, 0, false, "?"), i); + } + + // Parse regular type with potential generics + ParseResult typeResult = parseTypeWithGenerics(expr, i); + if (typeResult == null) { + return null; + } + + i = typeResult.endIndex; + ParsedType baseType = typeResult.type; + + // Skip whitespace + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + + // Check for array suffix + int arrayDims = 0; + while (i + 1 < len && expr.charAt(i) == '[' && expr.charAt(i + 1) == ']') { + arrayDims++; + i += 2; + // Skip whitespace + while (i < len && Character.isWhitespace(expr.charAt(i))) { + i++; + } + } + + // Check for nullable suffix + boolean isNullable = false; + if (i < len && expr.charAt(i) == '?') { + isNullable = true; + i++; + } + + // If we have array dims or nullable, wrap the base type + if (arrayDims > 0 || isNullable) { + return new ParseResult( + new ParsedType(baseType.baseName, baseType.typeArgs, arrayDims > 0, arrayDims, isNullable, + expr.substring(startIndex, i)), + i + ); + } + + return new ParseResult(baseType, i); + } + + /** + * Convenience method to check if a type string contains generic arguments. + */ + public static boolean hasGenerics(String typeExpr) { + return typeExpr != null && typeExpr.contains("<") && typeExpr.contains(">"); + } + + /** + * Strip generic arguments from a type string (for base type lookup). + * Example: "List" -> "List" + */ + public static String stripGenerics(String typeExpr) { + if (typeExpr == null) return null; + int idx = typeExpr.indexOf('<'); + if (idx > 0) { + return typeExpr.substring(0, idx).trim(); + } + return typeExpr; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index 500c6afa2..fd2eaec77 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -11,6 +11,11 @@ import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; import java.lang.reflect.Constructor; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.List; @@ -63,20 +68,34 @@ public enum Kind { // JavaScript/TypeScript type info (for types from .d.ts files) private final JSTypeInfo jsTypeInfo; // The JS type info (null if Java type) - // Type parameters (generics) + // Declared type parameters (generics definition, e.g., "T extends Entity" on interface List) private final List typeParams = new ArrayList<>(); + + // Applied type arguments (concrete types provided for generics, e.g., in Map) + // These are TypeInfo instances that can themselves be parameterized for nested generics + private final List appliedTypeArgs = new ArrayList<>(); + + // For parameterized types, the raw/base type (e.g., List for List) + // Null for non-parameterized types + private final TypeInfo rawType; // Documentation (script-defined types) private JSDocInfo jsDocInfo; private TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { - this(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, null); + this(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, null, null, null); } private TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, JSTypeInfo jsTypeInfo) { + this(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, jsTypeInfo, null, null); + } + + private TypeInfo(String simpleName, String fullName, String packageName, + Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType, + JSTypeInfo jsTypeInfo, TypeInfo rawType, List appliedTypeArgs) { this.simpleName = simpleName; this.fullName = fullName; this.packageName = packageName; @@ -85,6 +104,10 @@ private TypeInfo(String simpleName, String fullName, String packageName, this.resolved = resolved; this.enclosingType = enclosingType; this.jsTypeInfo = jsTypeInfo; + this.rawType = rawType; + if (appliedTypeArgs != null) { + this.appliedTypeArgs.addAll(appliedTypeArgs); + } } // Protected constructor for subclasses (like ScriptTypeInfo) @@ -99,6 +122,7 @@ protected TypeInfo(String simpleName, String fullName, String packageName, this.resolved = resolved; this.enclosingType = enclosingType; this.jsTypeInfo = null; + this.rawType = null; } // Factory methods @@ -151,6 +175,94 @@ public static TypeInfo fromClass(Class clazz) { return new TypeInfo(simpleName, fullName, packageName, kind, clazz, true, enclosing); } + /** + * Create a TypeInfo from a generic java.lang.reflect.Type. + * This preserves generic type arguments from reflection APIs like + * Method.getGenericReturnType() and Field.getGenericType(). + * + * Examples: + * - List -> TypeInfo(List) with appliedTypeArgs=[TypeInfo(String)] + * - Map -> TypeInfo(Map) with appliedTypeArgs=[TypeInfo(String), TypeInfo(Integer)] + * - T (type variable) -> TypeInfo representing the type variable name + * + * @param type The generic type from reflection + * @return A TypeInfo preserving the generic structure, or null if type is null + */ + public static TypeInfo fromGenericType(Type type) { + if (type == null) return null; + + if (type instanceof Class) { + // Plain class - no generic info + return fromClass((Class) type); + } + + if (type instanceof ParameterizedType) { + // Generic type like List + ParameterizedType paramType = (ParameterizedType) type; + Type rawType = paramType.getRawType(); + + if (!(rawType instanceof Class)) { + // Edge case - should not happen normally + return rawType != null ? fromGenericType(rawType) : null; + } + + // Get the raw type as TypeInfo + TypeInfo rawTypeInfo = fromClass((Class) rawType); + + // Recursively convert type arguments + Type[] typeArgs = paramType.getActualTypeArguments(); + List appliedArgs = new ArrayList<>(typeArgs.length); + for (Type arg : typeArgs) { + TypeInfo argInfo = fromGenericType(arg); + if (argInfo != null) { + appliedArgs.add(argInfo); + } + } + + // Return a parameterized TypeInfo + if (!appliedArgs.isEmpty()) { + return rawTypeInfo.parameterize(appliedArgs); + } + return rawTypeInfo; + } + + if (type instanceof GenericArrayType) { + // Array of generic type, e.g. T[] or List[] + GenericArrayType arrayType = (GenericArrayType) type; + TypeInfo elementType = fromGenericType(arrayType.getGenericComponentType()); + if (elementType != null) { + return arrayOf(elementType); + } + return fromClass(Object[].class); + } + + if (type instanceof TypeVariable) { + // Type variable like T, E, K, V + TypeVariable typeVar = (TypeVariable) type; + String varName = typeVar.getName(); + // Create an unresolved type representing the type variable + // The substitution system will handle replacing this with the actual type + return TypeInfo.unresolved(varName, varName); + } + + if (type instanceof WildcardType) { + // Wildcard like ? or ? extends Number or ? super Integer + WildcardType wildcard = (WildcardType) type; + Type[] upperBounds = wildcard.getUpperBounds(); + + // Use the upper bound if available (most useful for ? extends T) + if (upperBounds.length > 0 && upperBounds[0] != Object.class) { + return fromGenericType(upperBounds[0]); + } + + // For ? super T or plain ?, just use Object + return fromClass(Object.class); + } + + // Unknown type - shouldn't happen in practice + return null; + } + /** * Create a TypeInfo for a primitive type. */ @@ -199,9 +311,10 @@ public static TypeInfo arrayOf(TypeInfo elementType) { if (elementType == null) { return fromClass(Object[].class); } - - String simpleName = elementType.getSimpleName() + "[]"; - String fullName = elementType.getFullName() + "[]"; + + // Preserve generic display in arrays (e.g., List[]) and keep JS namespace display. + String simpleName = elementType.getDisplayName() + "[]"; + String fullName = elementType.getDisplayNameFull() + "[]"; String pkg = elementType.getPackageName(); // Try to get the actual array class if we have a Java class @@ -253,6 +366,125 @@ public JSDocInfo getJSDocInfo() { public boolean isEnum() {return kind == Kind.ENUM;} public boolean isPrimitive() {return javaClass != null && javaClass.isPrimitive();} + // ==================== Applied Type Arguments (Parameterized Types) ==================== + + /** + * Check if this type is parameterized (has applied type arguments). + * For example, List is parameterized, but List is not. + */ + public boolean isParameterized() { + return !appliedTypeArgs.isEmpty(); + } + + /** + * Get the raw/base type if this is parameterized. + * For List, returns the TypeInfo for List. + * For non-parameterized types, returns this. + */ + public TypeInfo getRawType() { + return rawType != null ? rawType : this; + } + + /** + * Get the applied type arguments. + * For List, returns [TypeInfo(String)]. + * For Map, returns [TypeInfo(String), TypeInfo(Integer)]. + * For nested generics like List>, the Map TypeInfo itself has appliedTypeArgs. + */ + public List getAppliedTypeArgs() { + return appliedTypeArgs; + } + + /** + * Create a parameterized version of this type with the given type arguments. + * For example, calling parameterize([TypeInfo(String)]) on List returns List. + * + * @param typeArgs The type arguments to apply + * @return A new parameterized TypeInfo + */ + public TypeInfo parameterize(List typeArgs) { + if (typeArgs == null || typeArgs.isEmpty()) { + return this; + } + + // Keep base names stable for logic (constructor names, lookups, etc.). + // Generic arguments are tracked via appliedTypeArgs and rendered via getDisplayName*(). + TypeInfo raw = getRawType(); + return new TypeInfo(raw.simpleName, raw.fullName, raw.packageName, raw.kind, raw.javaClass, + raw.resolved, raw.enclosingType, raw.jsTypeInfo, raw, typeArgs); + } + + /** + * Create a parameterized version of this type with a single type argument. + */ + public TypeInfo parameterize(TypeInfo typeArg) { + if (typeArg == null) { + return this; + } + List args = new ArrayList<>(); + args.add(typeArg); + return parameterize(args); + } + + /** + * Build the generic type arguments string like "". + * @param typeArgs The type arguments + * @param useFullNames Whether to use full names (for fullName) or simple names (for simpleName) + */ + private static String buildTypeArgsString(List typeArgs, boolean useFullNames) { + if (typeArgs == null || typeArgs.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder("<"); + for (int i = 0; i < typeArgs.size(); i++) { + if (i > 0) sb.append(", "); + TypeInfo arg = typeArgs.get(i); + sb.append(useFullNames ? arg.getDisplayNameFull() : arg.getDisplayNameSimple()); + } + sb.append(">"); + return sb.toString(); + } + + /** + * Get the canonical display name for this type, including generic arguments. + * This is the preferred method for UI display (hover, autocomplete, etc.). + * + * For Java types: uses simple name + generics (e.g., "List") + * For JS types: uses full name + generics (e.g., "IPlayerEvent.InteractEvent") + * For arrays: includes "[]" suffix + * For nested generics: recursively builds (e.g., "List>") + */ + public String getDisplayName() { + // Use full name for JS types, simple name for Java + return isJSType() ? getDisplayNameFull() : getDisplayNameSimple(); + } + + /** + * Get display name using simple names for all types. + * Example: "Map" instead of "java.util.Map" + */ + public String getDisplayNameSimple() { + if (appliedTypeArgs.isEmpty()) { + return simpleName; + } + + String baseName = rawType != null ? rawType.getSimpleName() : simpleName; + return baseName + buildTypeArgsString(appliedTypeArgs, false); + } + + /** + * Get display name using full qualified names. + * Example: "java.util.Map" + */ + public String getDisplayNameFull() { + if (appliedTypeArgs.isEmpty()) { + return fullName; + } + // For full display, use the raw type's full name + the args with their display names + String baseName = rawType != null ? rawType.getFullName() : fullName; + return baseName + buildTypeArgsString(appliedTypeArgs, true); + } + /** * Returns true if this TypeInfo represents a Class reference (not an instance). * For example, Java.type("java.io.File") returns a Class reference. @@ -729,6 +961,21 @@ public List getTypeParams() { if (jsTypeInfo != null) { return jsTypeInfo.getTypeParams(); } + + // For Java reflection types, lazily expose declared generic parameters (e.g., List, Map) + if (javaClass != null && typeParams.isEmpty()) { + try { + TypeVariable[] vars = javaClass.getTypeParameters(); + for (TypeVariable v : vars) { + if (v == null) continue; + String n = v.getName(); + if (n == null || n.isEmpty()) continue; + typeParams.add(new TypeParamInfo(n, null, null)); + } + } catch (Exception ignored) { + // Best-effort only; leave typeParams empty. + } + } return typeParams; } 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 53aae5b06..7ba64041e 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 @@ -314,8 +314,9 @@ public TypeInfo resolve(String typeName) { /** * Resolve a type name for JavaScript context. * Handles JS primitives, .d.ts types, and falls back to Java types. + * NOW preserves generic type arguments in the returned TypeInfo. * - * @param jsTypeName The JS type name (e.g., "IPlayer", "string", "number", "Java.java.io.File") + * @param jsTypeName The JS type name (e.g., "IPlayer", "string", "List", "Map") * @return TypeInfo for the resolved type, or unresolved TypeInfo if not found */ public TypeInfo resolveJSType(String jsTypeName) { @@ -323,43 +324,79 @@ public TypeInfo resolveJSType(String jsTypeName) { return TypeInfo.fromPrimitive("void"); } - // Handle union types - use first type - if (jsTypeName.contains("|")) { - jsTypeName = jsTypeName.split("\\|")[0].trim(); + // Use the generic type parser for full handling + GenericTypeParser.ParsedType parsed = GenericTypeParser.parse(jsTypeName); + if (parsed == null) { + return TypeInfo.unresolved(jsTypeName, jsTypeName); } - - // Handle nullable - jsTypeName = jsTypeName.replace("?", "").trim(); - // Strip array brackets for base type resolution - boolean isArray = jsTypeName.endsWith("[]"); - String baseName = isArray ? jsTypeName.substring(0, jsTypeName.length() - 2) : jsTypeName; - - // Check synthetic types (Nashorn built-ins, custom types, etc.) - if (isSyntheticType(baseName)) { - SyntheticType syntheticType = getSyntheticType(baseName); - return syntheticType.getTypeInfo(); + // Resolve the parsed type tree into a TypeInfo tree + return resolveFromParsedType(parsed); + } + + /** + * Resolve a ParsedType tree into a TypeInfo, preserving generic arguments. + * This is the core resolution method that handles nested generics. + */ + private TypeInfo resolveFromParsedType(GenericTypeParser.ParsedType parsed) { + if (parsed == null) { + return null; } + String baseName = parsed.baseName; + // Handle "Java." prefix - convert to actual Java type - // e.g., "Java.java.io.File" -> "java.io.File" if (baseName.startsWith("Java.")) { - String javaFullName = baseName.substring(5); // Remove "Java." - TypeInfo javaType = resolveFullName(javaFullName); - if (javaType != null && javaType.isResolved()) { - return isArray ? TypeInfo.arrayOf(javaType) : javaType; + baseName = baseName.substring(5); // Remove "Java." + } + + // Resolve the base type (without generics) + TypeInfo baseType = resolveBaseType(baseName); + + // If we have type arguments, recursively resolve them and create parameterized type + if (parsed.hasTypeArgs() && baseType != null && baseType.isResolved()) { + List resolvedArgs = new ArrayList<>(); + for (GenericTypeParser.ParsedType argParsed : parsed.typeArgs) { + TypeInfo argType = resolveFromParsedType(argParsed); + resolvedArgs.add(argType != null ? argType : TypeInfo.unresolved(argParsed.baseName, argParsed.baseName)); } - // Still return unresolved with the cleaned-up name - return TypeInfo.unresolved(javaFullName.substring(javaFullName.lastIndexOf('.') + 1), javaFullName); + baseType = baseType.parameterize(resolvedArgs); + } + + // Handle array + if (parsed.isArray && baseType != null) { + for (int i = 0; i < parsed.arrayDimensions; i++) { + baseType = TypeInfo.arrayOf(baseType); + } + } + + // Return the resolved (possibly parameterized) type + return baseType != null ? baseType : TypeInfo.unresolved(parsed.baseName, parsed.rawString); + } + + /** + * Resolve a base type name (without generics) to a TypeInfo. + * This handles primitives, synthetic types, JS registry types, and Java types. + */ + private TypeInfo resolveBaseType(String baseName) { + if (baseName == null || baseName.isEmpty()) { + return null; + } + + // Check synthetic types (Nashorn built-ins, custom types, etc.) + if (isSyntheticType(baseName)) { + SyntheticType syntheticType = getSyntheticType(baseName); + return syntheticType.getTypeInfo(); } + // Check primitives and common types switch (baseName.toLowerCase()) { case "string": return TypeInfo.STRING; case "number": case "int": case "integer": - return TypeInfo.NUMBER; + return TypeInfo.NUMBER; case "boolean": case "bool": return TypeInfo.BOOLEAN; @@ -374,18 +411,17 @@ public TypeInfo resolveJSType(String jsTypeName) { // Check JS type registry JSTypeInfo jsTypeInfo = getJSTypeRegistry().getType(baseName); if (jsTypeInfo != null) { - TypeInfo baseType = TypeInfo.fromJSTypeInfo(jsTypeInfo); - return isArray ? TypeInfo.arrayOf(baseType) : baseType; + return TypeInfo.fromJSTypeInfo(jsTypeInfo); } // Fall back to Java type resolution TypeInfo javaType = resolveFullName(baseName); if (javaType != null && javaType.isResolved()) { - return isArray ? TypeInfo.arrayOf(javaType) : javaType; + return javaType; } // Unresolved - return TypeInfo.unresolved(jsTypeName, jsTypeName); + return TypeInfo.unresolved(baseName, baseName); } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java new file mode 100644 index 000000000..3eed427fd --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java @@ -0,0 +1,222 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility for substituting type variables (like T, E, K, V) with concrete types. + * + * Used when resolving members on parameterized types: + * - List.get(int) returns String (not T/Object) + * - Map.get(Object) returns Integer (not V) + * + * Handles nested generics: + * - List>.get(0) returns Map + */ +public class TypeSubstitutor { + + /** + * Create a binding map from declared type parameters to applied type arguments. + * + * Example: For List where List is declared as interface List: + * - declaredParams = [TypeParamInfo("E", ...)] + * - appliedArgs = [TypeInfo(String)] + * - Result: {"E" -> TypeInfo(String)} + * + * @param declaredParams The declared type parameters (from the generic type definition) + * @param appliedArgs The applied type arguments (from the parameterized usage) + * @return A map from type parameter names to their concrete TypeInfo values + */ + public static Map createBindings(List declaredParams, List appliedArgs) { + Map bindings = new HashMap<>(); + + if (declaredParams == null || appliedArgs == null) { + return bindings; + } + + int count = Math.min(declaredParams.size(), appliedArgs.size()); + for (int i = 0; i < count; i++) { + String paramName = declaredParams.get(i).getName(); + TypeInfo argType = appliedArgs.get(i); + if (paramName != null && argType != null) { + bindings.put(paramName, argType); + } + } + + return bindings; + } + + /** + * Create bindings from a parameterized receiver type. + * + * @param receiverType The parameterized type (e.g., List) + * @return Bindings from the type's declared params to its applied args + */ + public static Map createBindingsFromReceiver(TypeInfo receiverType) { + if (receiverType == null) { + return new HashMap<>(); + } + + // Get the raw type to access declared type parameters + TypeInfo rawType = receiverType.getRawType(); + List declaredParams = null; + + // Get declared params from JSTypeInfo or reflection + if (rawType.isJSType() && rawType.getJSTypeInfo() != null) { + declaredParams = rawType.getJSTypeInfo().getTypeParams(); + } else { + declaredParams = rawType.getTypeParams(); + } + + // Get applied args from the parameterized type + List appliedArgs = receiverType.getAppliedTypeArgs(); + + return createBindings(declaredParams, appliedArgs); + } + + /** + * Substitute type variables in a type using the given bindings. + * + * @param type The type to substitute (may contain type variables like T, E) + * @param bindings The map from type variable names to concrete types + * @return The substituted type + */ + public static TypeInfo substitute(TypeInfo type, Map bindings) { + if (type == null || bindings == null || bindings.isEmpty()) { + return type; + } + + String typeName = type.getSimpleName(); + + // Check if this type is itself a type variable + if (!type.isResolved() || isTypeVariable(typeName)) { + TypeInfo substitution = bindings.get(typeName); + if (substitution != null) { + return substitution; + } + } + + // If the type has applied type arguments, substitute within them recursively + if (type.isParameterized()) { + List originalArgs = type.getAppliedTypeArgs(); + List substitutedArgs = new ArrayList<>(); + boolean changed = false; + + for (TypeInfo arg : originalArgs) { + TypeInfo substitutedArg = substitute(arg, bindings); + substitutedArgs.add(substitutedArg); + if (substitutedArg != arg) { + changed = true; + } + } + + if (changed) { + // Create a new parameterized type with substituted args + return type.getRawType().parameterize(substitutedArgs); + } + } + + // Handle array types - substitute the element type + if (type.getSimpleName().endsWith("[]")) { + // Extract element type and substitute + String elementTypeName = typeName.substring(0, typeName.length() - 2); + TypeInfo elementSubstitution = bindings.get(elementTypeName); + if (elementSubstitution != null) { + return TypeInfo.arrayOf(elementSubstitution); + } + } + + return type; + } + + /** + * Substitute type variables in a type string using the given bindings. + * Used when we have a raw type string (like "T" or "List") that needs substitution. + * + * @param typeString The raw type string + * @param bindings The map from type variable names to concrete types + * @param resolver The type resolver to use for parsing + * @return The substituted TypeInfo + */ + public static TypeInfo substituteString(String typeString, Map bindings, TypeResolver resolver) { + if (typeString == null || typeString.isEmpty()) { + return null; + } + + // First check if the whole string is a type variable + String trimmed = typeString.trim(); + + // Handle array suffix + boolean isArray = trimmed.endsWith("[]"); + String baseName = isArray ? trimmed.substring(0, trimmed.length() - 2).trim() : trimmed; + + // Strip generic args to check the base name + String bareBaseName = GenericTypeParser.stripGenerics(baseName); + + // If bare base name is a type variable, substitute + TypeInfo directSubstitution = bindings.get(bareBaseName); + if (directSubstitution != null) { + // It's a type variable, return the substitution + TypeInfo result = directSubstitution; + if (isArray) { + result = TypeInfo.arrayOf(result); + } + return result; + } + + // Parse and resolve the type, then substitute within it + TypeInfo resolved = resolver.resolveJSType(typeString); + return substitute(resolved, bindings); + } + + /** + * Check if a type name looks like a type variable (single uppercase letter or short uppercase name). + */ + private static boolean isTypeVariable(String name) { + if (name == null || name.isEmpty()) { + return false; + } + // Common type variable patterns: T, E, K, V, R, U, etc. + // Also handles longer ones like KEY, VALUE but those are rare + return name.length() <= 3 && + Character.isUpperCase(name.charAt(0)) && + name.chars().allMatch(c -> Character.isUpperCase(c) || Character.isDigit(c)); + } + + /** + * Get the return type for a method on a parameterized receiver, with type variable substitution. + * + * @param rawReturnType The method's raw return type (may contain type variables) + * @param rawReturnTypeString The method's raw return type as a string (for unresolved types) + * @param receiverType The parameterized receiver type + * @param resolver The type resolver + * @return The substituted return type + */ + public static TypeInfo getSubstitutedReturnType(TypeInfo rawReturnType, String rawReturnTypeString, + TypeInfo receiverType, TypeResolver resolver) { + Map bindings = createBindingsFromReceiver(receiverType); + if (bindings.isEmpty()) { + return rawReturnType; + } + + // Try substituting the resolved type first + TypeInfo substituted = substitute(rawReturnType, bindings); + if (substituted != rawReturnType && substituted.isResolved()) { + return substituted; + } + + // If still not resolved, try substituting from the string + if (rawReturnTypeString != null && !rawReturnTypeString.isEmpty()) { + TypeInfo fromString = substituteString(rawReturnTypeString, bindings, resolver); + if (fromString != null && fromString.isResolved()) { + return fromString; + } + } + + return substituted; + } +} From 19cdf19a9b1ae5ce2a2301f266918b9c7ed4efed Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 27 Jan 2026 19:36:06 +0200 Subject: [PATCH 312/337] wtf --- .../controllers/data/ability/Ability.java | 13 ++++++++++++ .../java/noppes/npcs/NoppesUtilPlayer.java | 2 +- .../scripted/event/player/DialogEvent.java | 20 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 65a4e8ac5..c3f6d86ed 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -47,6 +47,7 @@ public abstract class Ability implements IAbility { // Timing (ticks) protected int cooldownTicks = 100; protected int windUpTicks = 20; + protected int dazedTicks = 0; protected int activeTicks = 10; protected int recoveryTicks = 20; @@ -588,6 +589,7 @@ public NBTTagCompound writeNBT() { nbt.setFloat("maxRange", maxRange); nbt.setInteger("cooldown", cooldownTicks); nbt.setInteger("windUp", windUpTicks); + nbt.setInteger("dazed", dazedTicks); nbt.setInteger("active", activeTicks); nbt.setInteger("recovery", recoveryTicks); nbt.setBoolean("interruptible", interruptible); @@ -627,6 +629,7 @@ public void readNBT(NBTTagCompound nbt) { maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); + dazedTicks = nbt.hasKey("dazed") ? nbt.getInteger("dazed") : 0; activeTicks = nbt.getInteger("active"); recoveryTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); @@ -763,6 +766,16 @@ public void setWindUpTicks(int windUpTicks) { this.windUpTicks = windUpTicks; } + @Override + public int getDazedTicks() { + return dazedTicks; + } + + @Override + public void setDazedTicks(int ticks) { + this.dazedTicks = ticks; + } + public int getActiveTicks() { return activeTicks; } diff --git a/src/main/java/noppes/npcs/NoppesUtilPlayer.java b/src/main/java/noppes/npcs/NoppesUtilPlayer.java index c78ffdf46..90e3c91ec 100644 --- a/src/main/java/noppes/npcs/NoppesUtilPlayer.java +++ b/src/main/java/noppes/npcs/NoppesUtilPlayer.java @@ -306,7 +306,7 @@ public static void dialogSelected(int dialogId, int optionId, EntityPlayerMP pla if (dialog == null) return; DialogOption option = dialog.options.get(optionId); - if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialog))) + if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialog, optionId))) return; if (!npc.isRemote()) { diff --git a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java index a5ebfc1ff..efc8caeab 100644 --- a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java @@ -8,16 +8,32 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { public final IDialog dialog; + private final int optionId; public DialogEvent(IPlayer player, IDialog dialog) { + this(player, dialog, -1); + } + + public DialogEvent(IPlayer player, IDialog dialog, int optionId) { super(player); this.dialog = dialog; + this.optionId = optionId; } public IDialog getDialog() { return dialog; } + @Override + public int getDialogId() { + return dialog != null ? dialog.getId() : -1; + } + + @Override + public int getOptionId() { + return optionId; + } + public String getHookName() { return EnumScriptType.DIALOG_EVENT.function; } @@ -39,6 +55,10 @@ public DialogOption(IPlayer player, IDialog dialog) { super(player, dialog); } + public DialogOption(IPlayer player, IDialog dialog, int optionId) { + super(player, dialog, optionId); + } + public String getHookName() { return EnumScriptType.DIALOG_OPTION.function; } From 125a799df4c584be878823682eecde1a7a09484b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 28 Jan 2026 04:37:17 +0200 Subject: [PATCH 313/337] Optimised and refined type generics parsing. Removed bloat and added edge case handling --- src/api | 2 +- .../script/interpreter/ScriptDocument.java | 183 ++++++------ .../script/interpreter/field/FieldInfo.java | 10 +- .../interpreter/js_parser/JSFieldInfo.java | 23 +- .../interpreter/js_parser/JSMethodInfo.java | 32 +-- .../js_parser/TypeScriptDefinitionParser.java | 51 +--- .../script/interpreter/method/MethodInfo.java | 18 +- .../interpreter/type/GenericContext.java | 269 ++++++++++++++++++ .../interpreter/type/GenericTypeParser.java | 153 ++-------- .../script/interpreter/type/TypeResolver.java | 105 ++++--- .../type/TypeStringNormalizer.java | 221 ++++++++++++++ .../interpreter/type/TypeSubstitutor.java | 65 ++--- 12 files changed, 722 insertions(+), 410 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeStringNormalizer.java diff --git a/src/api b/src/api index 2e5d86993..2054012fa 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2e5d86993cd47e6df4572323514d4fe2166c570c +Subproject commit 2054012fae799fa233b99824b4f649d311eff86f 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 516f68d57..d3c9c19e3 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 @@ -27,6 +27,7 @@ import noppes.npcs.controllers.data.DataScript; import java.util.*; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -1916,17 +1917,98 @@ private TypeInfo resolveTypeAndTrackUsage(String typeName) { if (typeName == null || typeName.isEmpty()) return TypeInfo.unresolved(typeName, typeName); - String normalized = typeName.trim(); - normalized = stripLeadingModifiers(normalized); + final String normalized = typeName.trim(); + final String normalizedFinal = stripLeadingModifiers(normalized); + + // Split array suffixes first + TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(normalizedFinal); + String baseExpr = arraySplit.base; + int arrayDims = arraySplit.dimensions; + + // Base type resolver with import tracking + Function resolveBase = baseName -> { + TypeInfo resolved; + + // Primitives + if (TypeResolver.isPrimitiveType(baseName)) { + resolved = TypeInfo.fromPrimitive(baseName); + } + // Common java.lang.String + else if ("String".equals(baseName)) { + resolved = typeResolver.resolveFullName("java.lang.String"); + } + // Script-defined types (simple names only) + else if (!baseName.contains(".") && scriptTypes.containsKey(baseName)) { + resolved = scriptTypes.get(baseName); + } + // Fully-qualified + else if (baseName.contains(".")) { + resolved = typeResolver.resolveFullName(baseName); + } + // Imported/simple + else { + resolved = typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); + + // Track import usage + if (resolved != null && resolved.isResolved()) { + ImportData usedImport = importsBySimpleName.get(baseName); + if (usedImport != null) { + usedImport.incrementUsage(); + } else if (wildcardPackages != null) { + String resultPkg = resolved.getPackageName(); + for (ImportData imp : imports) { + if (imp.isWildcard() && resultPkg != null && resultPkg.equals(imp.getFullPath())) { + imp.incrementUsage(); + break; + } + } + } + } + } + + return resolved != null ? resolved : TypeInfo.unresolved(baseName, normalizedFinal); + }; + + TypeInfo resolved; - // Preserve and resolve nested generics (e.g., List>) - GenericTypeParser.ParsedType parsed = GenericTypeParser.parse(normalized); - if (parsed == null) { - return TypeInfo.unresolved(normalized, normalized); + // Fast path: no generics - skip expensive parsing + if (!baseExpr.contains("<")) { + resolved = resolveBase.apply(baseExpr); + } else { + // Slow path: parse and resolve generics + GenericTypeParser.ParsedType parsed = GenericTypeParser.parse(baseExpr); + if (parsed != null) { + // Normalize whitespace around dots in base name + String baseName = parsed.baseName.replaceAll("\\s*\\.\\s*", ".").trim(); + resolved = resolveBase.apply(baseName); + + // Apply generic arguments + if (parsed.hasTypeArgs() && resolved != null && resolved.isResolved()) { + List resolvedArgs = new ArrayList<>(); + for (GenericTypeParser.ParsedType argParsed : parsed.typeArgs) { + if (argParsed == null) { + resolvedArgs.add(TypeInfo.fromClass(Object.class)); + } else { + TypeInfo argType = resolveTypeAndTrackUsage(argParsed.rawString); + resolvedArgs.add(argType != null ? argType : TypeInfo.unresolved(argParsed.baseName, + argParsed.baseName)); + } + } + if (!resolvedArgs.isEmpty()) { + resolved = resolved.parameterize(resolvedArgs); + } + } + } else + // Fallback: treat as simple type + resolved = resolveBase.apply(baseExpr); + } + + // Apply array dimensions + for (int i = 0; i < arrayDims; i++) { + resolved = TypeInfo.arrayOf(resolved); } - TypeInfo result = resolveJavaFromParsedType(parsed); - return result != null ? result : TypeInfo.unresolved(parsed.baseName, normalized); + return resolved; } /** @@ -1957,91 +2039,6 @@ private String stripLeadingModifiers(String typeExpr) { return typeExpr.trim(); } - /** - * Resolve a GenericTypeParser ParsedType for Java/Groovy scripts, preserving type arguments. - * Also tracks import usage for base types and nested type arguments. - */ - private TypeInfo resolveJavaFromParsedType(GenericTypeParser.ParsedType parsed) { - if (parsed == null) { - return null; - } - - String baseName = parsed.baseName; - if (baseName == null || baseName.isEmpty()) { - return null; - } - - // Normalize whitespace around dots - baseName = baseName.replaceAll("\\s*\\.\\s*", ".").trim(); - - TypeInfo baseType; - - // Primitives - if (TypeResolver.isPrimitiveType(baseName)) { - baseType = TypeInfo.fromPrimitive(baseName); - } - // Common java.lang.String - else if ("String".equals(baseName)) { - baseType = typeResolver.resolveFullName("java.lang.String"); - } - // Script-defined types (simple names only) - else if (!baseName.contains(".") && scriptTypes.containsKey(baseName)) { - baseType = scriptTypes.get(baseName); - } - // Fully-qualified - else if (baseName.contains(".")) { - baseType = typeResolver.resolveFullName(baseName); - } - // Imported/simple - else { - baseType = typeResolver.resolveSimpleName(baseName, importsBySimpleName, wildcardPackages); - - // Track explicit import usage if present - ImportData usedImport = importsBySimpleName.get(baseName); - if (baseType != null && baseType.isResolved() && usedImport != null) { - usedImport.incrementUsage(); - } - - // Track wildcard import usage if resolved through a wildcard - if (baseType != null && baseType.isResolved() && usedImport == null && wildcardPackages != null) { - String resultPkg = baseType.getPackageName(); - for (ImportData imp : imports) { - if (imp.isWildcard() && resultPkg != null && resultPkg.equals(imp.getFullPath())) { - imp.incrementUsage(); - break; - } - } - } - } - - if (baseType == null) { - baseType = TypeInfo.unresolved(baseName, baseName); - } - - // Apply generic arguments - if (parsed.hasTypeArgs() && baseType.isResolved()) { - java.util.List resolvedArgs = new java.util.ArrayList<>(); - for (GenericTypeParser.ParsedType argParsed : parsed.typeArgs) { - TypeInfo argType = resolveJavaFromParsedType(argParsed); - resolvedArgs.add(argType != null ? argType : TypeInfo.unresolved(argParsed.baseName, argParsed.baseName)); - } - if (!resolvedArgs.isEmpty()) { - baseType = baseType.parameterize(resolvedArgs); - } - } - - // Apply array dimensions - if (parsed.isArray) { - TypeInfo arrayType = baseType; - for (int i = 0; i < parsed.arrayDimensions; i++) { - arrayType = TypeInfo.arrayOf(arrayType); - } - baseType = arrayType; - } - - return baseType; - } - // ==================== PHASE 4: BUILD MARKS ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index e4239074a..2c021920a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -6,6 +6,7 @@ import noppes.npcs.client.gui.util.script.interpreter.bridge.DtsJavaBridge; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo; @@ -16,6 +17,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; /** * Metadata for a field (variable) declaration or reference. @@ -158,9 +160,11 @@ public static FieldInfo fromReflection(Field field, TypeInfo containingType) { } // If the receiver is parameterized, substitute class type variables in the field type - java.util.Map receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); - if (!receiverBindings.isEmpty()) { - type = TypeSubstitutor.substitute(type, receiverBindings); + if (GenericContext.hasGenerics(containingType)) { + Map receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); + if (!receiverBindings.isEmpty()) { + type = TypeSubstitutor.substitute(type, receiverBindings); + } } // Check if this is an enum constant diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java index c09a1e2b8..7cec4dd6c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSFieldInfo.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; /** @@ -44,19 +45,17 @@ public void setTypeInfo(TypeInfo typeInfo) { * @return The resolved TypeInfo for the field type */ public TypeInfo getResolvedType(TypeInfo contextType) { - // Use pre-resolved if available - if (typeInfo != null && typeInfo.isResolved()) - return typeInfo; - - // Fall back to resolving from string TypeResolver resolver = TypeResolver.getInstance(); - TypeInfo resolved = typeInfo = resolver.resolveJSType(type); - - // If not resolved and contextType has type parameters, try to resolve as type parameter - if (contextType != null && !resolved.isResolved()) { - TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(type); - if (paramResolution != null) - resolved = typeInfo = paramResolution; // cache it to typeInfo + + // Cache only the raw resolved type; substitutions depend on the receiver context. + TypeInfo resolved = typeInfo != null ? typeInfo : resolver.resolveJSType(type); + if (typeInfo == null) { + typeInfo = resolved; + } + + if (contextType != null) { + GenericContext ctx = GenericContext.forReceiver(contextType); + return ctx.substituteType(resolved, type, resolver); } return resolved; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java index e8c1d7f2b..bfb783849 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/js_parser/JSMethodInfo.java @@ -1,9 +1,9 @@ package noppes.npcs.client.gui.util.script.interpreter.js_parser; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver; -import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; import java.util.*; @@ -59,20 +59,11 @@ public void setContainingType(JSTypeInfo containingType) { */ public TypeInfo getResolvedReturnType(TypeInfo contextType) { TypeResolver resolver = TypeResolver.getInstance(); - - // First, resolve the return type string to a TypeInfo (now preserves generic args) TypeInfo resolved = resolver.resolveJSType(returnType); - // If we have a parameterized context, apply type variable substitution - if (contextType != null && contextType.isParameterized()) { - resolved = TypeSubstitutor.getSubstitutedReturnType(resolved, returnType, contextType, resolver); - } - // Fallback: if not resolved and contextType has declared type parameters, try direct resolution - else if (contextType != null && !resolved.isResolved()) { - TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(returnType); - if (paramResolution != null) { - resolved = paramResolution; - } + if (contextType != null) { + GenericContext ctx = GenericContext.forReceiver(contextType); + resolved = ctx.substituteType(resolved, returnType, resolver); } return resolved; @@ -168,20 +159,11 @@ public void setTypeInfo(TypeInfo typeInfo) { */ public TypeInfo getResolvedType(TypeInfo contextType) { TypeResolver resolver = TypeResolver.getInstance(); - - // First, resolve the type string to a TypeInfo (now preserves generic args) TypeInfo resolved = resolver.resolveJSType(type); - // If we have a parameterized context, apply type variable substitution - if (contextType != null && contextType.isParameterized()) { - resolved = TypeSubstitutor.getSubstitutedReturnType(resolved, type, contextType, resolver); - } - // Fallback: if not resolved and contextType has declared type parameters, try direct resolution - else if (contextType != null && !resolved.isResolved()) { - TypeInfo paramResolution = contextType.resolveTypeParamToTypeInfo(type); - if (paramResolution != null) { - resolved = paramResolution; - } + if (contextType != null) { + GenericContext ctx = GenericContext.forReceiver(contextType); + resolved = ctx.substituteType(resolved, type, resolver); } return resolved; 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 b4c26f69c..a0798da25 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 @@ -2,6 +2,7 @@ import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo; import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTag; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeStringNormalizer; import java.io.*; import java.util.*; @@ -665,55 +666,7 @@ private List splitParameters(String params) { */ private String cleanType(String type) { if (type == null) return "any"; - type = type.trim(); - - // Strip TypeScript import() type references anywhere in the string. - // Examples: - // - import('./data/IAction').IAction -> IAction - // - Java.java.util.function.Consumer -> Java.java.util.function.Consumer - // This is important because import() can appear nested inside generics. - int importIdx; - while ((importIdx = type.indexOf("import(")) >= 0) { - int i = importIdx + "import(".length(); - int depth = 1; - boolean inString = false; - char stringChar = 0; - - while (i < type.length() && depth > 0) { - char c = type.charAt(i); - if (inString) { - if (c == stringChar && (i == 0 || type.charAt(i - 1) != '\\')) { - inString = false; - } - } else { - if (c == '\'' || c == '"') { - inString = true; - stringChar = c; - } else if (c == '(') { - depth++; - } else if (c == ')') { - depth--; - } - } - i++; - } - - // i is positioned just after the matching ')', if found. - if (depth != 0) { - break; // Unbalanced import( ... ) - stop trying to clean. - } - - // If immediately followed by ".", remove "import(...) ." prefix. - if (i < type.length() && type.charAt(i) == '.') { - int removeEnd = i + 1; - type = type.substring(0, importIdx) + type.substring(removeEnd); - } else { - // Remove just the import(...) portion. - type = type.substring(0, importIdx) + type.substring(i); - } - } - - return type; + return TypeStringNormalizer.stripImportTypeSyntax(type); } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 1315ff72a..886b1bb54 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -8,6 +8,7 @@ import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo; import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry; import noppes.npcs.client.gui.util.script.interpreter.token.TokenType; +import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext; import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor; @@ -15,11 +16,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; /** * Metadata for a method declaration or method call. @@ -174,9 +171,12 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) } // If the receiver is parameterized (e.g., List), substitute class type variables (E -> String) - java.util.Map receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); - if (!receiverBindings.isEmpty()) { - returnType = TypeSubstitutor.substitute(returnType, receiverBindings); + Map receiverBindings = null; + if (GenericContext.hasGenerics(containingType)) { + receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType); + if (!receiverBindings.isEmpty()) { + returnType = TypeSubstitutor.substitute(returnType, receiverBindings); + } } List params = new ArrayList<>(); // Use getGenericParameterTypes() to preserve generic information @@ -188,7 +188,7 @@ public static MethodInfo fromReflection(Method method, TypeInfo containingType) paramType = TypeInfo.fromClass(method.getParameterTypes()[i]); } - if (!receiverBindings.isEmpty()) { + if (receiverBindings != null && !receiverBindings.isEmpty()) { paramType = TypeSubstitutor.substitute(paramType, receiverBindings); } String paramName = "arg" + i; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java new file mode 100644 index 000000000..1d1162ac9 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericContext.java @@ -0,0 +1,269 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A context for resolving and substituting declared type parameters (T, E, K, V...) + * based on a receiver type. + * + * Policy: + * - Prefer applied type arguments when present (e.g. List binds E -> String). + * - Fall back to declared bounds when no applied argument exists (e.g. T extends Entity -> Entity). + */ +public final class GenericContext { + + /** Singleton context for types with no generic parameters. Used to avoid object allocation. */ + private static final GenericContext EMPTY = new GenericContext(new HashMap<>(), new HashMap<>()); + + /** + * Map of type variable names to their applied type arguments. + * For example, in List<String>, this contains E -> TypeInfo.STRING. + * Applied bindings take precedence over bound fallbacks. + */ + private final Map appliedBindings; + + /** + * Map of type variable names to their declared upper bounds. + * For example, in <T extends Entity>, this contains T -> TypeInfo(Entity). + * Used as fallback when no applied argument exists. + */ + private final Map boundFallbacks; + + private GenericContext(Map appliedBindings, Map boundFallbacks) { + this.appliedBindings = appliedBindings != null ? appliedBindings : new HashMap<>(); + this.boundFallbacks = boundFallbacks != null ? boundFallbacks : new HashMap<>(); + } + + /** + * Create a GenericContext for a receiver type. + * + * Fast path: returns singleton EMPTY for non-generic types (no allocation overhead). + * Slow path: builds maps from type parameters and applied arguments for generic types. + * + * @param receiverType the type to extract generic bindings from (e.g., List<String>, DAO<T extends Entity>) + * @return a context with applied bindings and bound fallbacks, or EMPTY singleton if receiverType is not generic + */ + public static GenericContext forReceiver(TypeInfo receiverType) { + // Fast path: return singleton for non-generic types + if (receiverType == null || !hasGenerics(receiverType)) { + return EMPTY; + } + + TypeInfo rawType = receiverType.getRawType(); + List declaredParams = rawType != null ? rawType.getTypeParams() : null; + List appliedArgs = receiverType.getAppliedTypeArgs(); + + Map applied = new HashMap<>(); + if (declaredParams != null && appliedArgs != null) { + int count = Math.min(declaredParams.size(), appliedArgs.size()); + for (int i = 0; i < count; i++) { + TypeParamInfo declared = declaredParams.get(i); + TypeInfo arg = appliedArgs.get(i); + if (declared == null || declared.getName() == null || declared.getName().isEmpty() || arg == null) { + continue; + } + applied.put(declared.getName(), arg); + } + } + + Map bounds = new HashMap<>(); + if (declaredParams != null) { + for (TypeParamInfo declared : declaredParams) { + if (declared == null) continue; + String name = declared.getName(); + if (name == null || name.isEmpty()) continue; + TypeInfo bound = declared.getBoundTypeInfo(); + if (bound != null && bound.isResolved()) { + bounds.put(name, bound); + } + } + } + + return new GenericContext(applied, bounds); + } + + /** + * Check if a type has generics that need substitution. + * + * Returns true if: + * - Type is parameterized (e.g., List<String>) + * - Type's raw form differs from the type itself + * - Type has declared type parameters + * + * @param type the type to check + * @return true if this type contains generic information that may need substitution + */ + public static boolean hasGenerics(TypeInfo type) { + return type.isParameterized() || + type.getRawType() != type || + (type.getTypeParams() != null && !type.getTypeParams().isEmpty()); + } + + /** + * Resolve a type variable to its bound or applied type. + * + * Prefers applied bindings (e.g., E -> String in List<String>) over declared bounds. + * Falls back to bounds (e.g., T -> Entity in <T extends Entity>) if no applied argument exists. + * Returns null if the variable is not found in either map. + * + * @param name the type variable name (e.g., "T", "E", "K") + * @return the resolved TypeInfo, or null if variable not found + */ + public TypeInfo resolveTypeVariable(String name) { + if (name == null || name.isEmpty()) { + return null; + } + + TypeInfo applied = appliedBindings.get(name); + if (applied != null) { + return applied; + } + + return boundFallbacks.get(name); + } + + /** + * Substitute type variables in a TypeInfo recursively. + * + * Handles: + * - Direct type variables (unresolved types representing T, E, etc.) + * - Parameterized types (recursively substitutes inside type arguments) + * - Array types (substitutes the element type, preserves array dimensions) + * + * @param type the type to substitute (may contain type variables) + * @return the substituted type, or the original type if no substitutions apply + */ + public TypeInfo substitute(TypeInfo type) { + if (type == null) { + return null; + } + + // Direct type variable substitution (unresolved types represent type variables like T). + if (!type.isResolved()) { + TypeInfo substitution = resolveTypeVariable(type.getSimpleName()); + if (substitution != null) { + return substitution; + } + } + + // Parameterized types: substitute inside args. + if (type.isParameterized()) { + List originalArgs = type.getAppliedTypeArgs(); + List substitutedArgs = new ArrayList<>(originalArgs.size()); + boolean changed = false; + + for (TypeInfo arg : originalArgs) { + TypeInfo substitutedArg = substitute(arg); + substitutedArgs.add(substitutedArg); + if (substitutedArg != arg) { + changed = true; + } + } + + if (changed) { + return type.getRawType().parameterize(substitutedArgs); + } + } + + // Array wrapper types are represented by a display-name suffix; we can still substitute + // a plain "T[]" by looking up "T". + String simple = type.getSimpleName(); + if (simple != null && simple.endsWith("[]")) { + TypeStringNormalizer.ArraySplit split = TypeStringNormalizer.splitArraySuffixes(simple); + String elementName = split.base; + int dims = split.dimensions; + TypeInfo elementSub = resolveTypeVariable(elementName); + if (elementSub != null) { + TypeInfo result = elementSub; + for (int i = 0; i < dims; i++) { + result = TypeInfo.arrayOf(result); + } + return result; + } + } + + return type; + } + + /** + * Substitute type variables in a type string. + * + * Attempts substitution in this order: + * 1. Normalize the string (strip imports, pick union branch, remove nullable suffix, split arrays) + * 2. Try direct type variable lookup (e.g., "T" -> resolved Entity) + * 3. Fall back to full type resolution with substitution applied + * + * @param typeString the type expression as a string (e.g., "T", "T[]", "List<T>") + * @param resolver optional TypeResolver for fallback resolution; if null, returns null on failure + * @return the substituted type, or null if resolution fails + */ + public TypeInfo substituteString(String typeString, TypeResolver resolver) { + if (typeString == null || typeString.trim().isEmpty()) { + return null; + } + + String normalized = TypeStringNormalizer.stripImportTypeSyntax(typeString); + normalized = TypeStringNormalizer.pickPreferredUnionBranch(normalized); + normalized = TypeStringNormalizer.stripNullableSuffix(normalized); + + TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(normalized); + String baseExpr = arraySplit.base; + int dims = arraySplit.dimensions; + + String bareBase = GenericTypeParser.stripGenerics(baseExpr); + TypeInfo direct = resolveTypeVariable(bareBase); + if (direct != null) { + TypeInfo result = direct; + for (int i = 0; i < dims; i++) { + result = TypeInfo.arrayOf(result); + } + return result; + } + + if (resolver == null) { + return null; + } + TypeInfo resolved = resolver.resolveJSType(typeString); + return substitute(resolved); + } + + /** + * Substitute type variables in a resolved type, with string fallback. + * + * Two-phase approach: + * 1. Try substituting the resolved type directly + * 2. If that fails, attempt substitution via the raw string as fallback + * + * This handles edge cases where the TypeInfo doesn't capture enough information + * to do proper substitution (e.g., union types, complex generics). + * + * @param resolvedType a TypeInfo that has already been resolved (may still contain type variables) + * @param rawTypeString the original type string before resolution (for fallback) + * @param resolver optional TypeResolver for string-based fallback resolution + * @return the substituted type, or the best attempt if full substitution fails + */ + public TypeInfo substituteType(TypeInfo resolvedType, String rawTypeString, TypeResolver resolver) { + if (resolvedType == null) { + return null; + } + + TypeInfo substituted = substitute(resolvedType); + if (substituted != null && substituted.isResolved()) { + return substituted; + } + + if (rawTypeString != null && !rawTypeString.trim().isEmpty()) { + TypeInfo fromString = substituteString(rawTypeString, resolver); + if (fromString != null && fromString.isResolved()) { + return fromString; + } + } + + return substituted; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java index 68a244179..727c85388 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/GenericTypeParser.java @@ -12,9 +12,9 @@ * - Single generic: "List", "Consumer" * - Multiple generics: "Map" * - Nested generics: "List>" - * - Arrays: "String[]", "List[]" - * - Nullable: "String?" - * - Union types: "String | null" (uses first type for resolution) + * + * Note: union/nullability/array semantics are handled by higher-level resolvers. + * This parser only consumes suffix tokens where needed to correctly delimit type arguments. * * This parser produces a ParsedType tree that can be resolved into parameterized TypeInfo instances. */ @@ -30,25 +30,12 @@ public static class ParsedType { /** The applied type arguments (for generics like List), each is itself a ParsedType */ public final List typeArgs; - /** Whether this type is an array (has [] suffix) */ - public final boolean isArray; - - /** Array dimensions (1 for T[], 2 for T[][], etc.) */ - public final int arrayDimensions; - - /** Whether this type is nullable (has ? suffix in TypeScript) */ - public final boolean isNullable; - /** The original raw string before parsing */ public final String rawString; - public ParsedType(String baseName, List typeArgs, boolean isArray, - int arrayDimensions, boolean isNullable, String rawString) { + public ParsedType(String baseName, List typeArgs, String rawString) { this.baseName = baseName; this.typeArgs = typeArgs != null ? typeArgs : new ArrayList<>(); - this.isArray = isArray; - this.arrayDimensions = arrayDimensions; - this.isNullable = isNullable; this.rawString = rawString; } @@ -73,12 +60,6 @@ public String toString() { } sb.append(">"); } - for (int i = 0; i < arrayDimensions; i++) { - sb.append("[]"); - } - if (isNullable) { - sb.append("?"); - } return sb.toString(); } } @@ -96,38 +77,14 @@ public static ParsedType parse(String typeExpr) { String expr = typeExpr.trim(); - // Handle union types - use first type (same as current behavior) - if (expr.contains("|")) { - String[] parts = expr.split("\\|"); - expr = parts[0].trim(); - } - - // Handle nullable marker (TypeScript ?), remove it but track - boolean isNullable = false; - if (expr.endsWith("?")) { - isNullable = true; - expr = expr.substring(0, expr.length() - 1).trim(); - } - - // Count and strip array dimensions - int arrayDims = 0; - while (expr.endsWith("[]")) { - arrayDims++; - expr = expr.substring(0, expr.length() - 2).trim(); - } - // Now parse the base type with potential generic arguments ParseResult result = parseTypeWithGenerics(expr, 0); if (result == null || result.type == null) { // Fallback: treat entire expression as a simple type - return new ParsedType(expr, null, arrayDims > 0, arrayDims, isNullable, typeExpr); + return new ParsedType(expr, null, typeExpr); } - ParsedType parsed = result.type; - - // Apply array and nullable modifiers - return new ParsedType(parsed.baseName, parsed.typeArgs, - arrayDims > 0, arrayDims, isNullable, typeExpr); + return result.type; } /** @@ -160,71 +117,18 @@ private static ParseResult parseTypeWithGenerics(String expr, int startIndex) { return null; } - // Special-case: TypeScript import() type references like import('./x').TypeName - // We normalize these to just the referenced type name (e.g., "TypeName" or "Ns.TypeName"). - String baseName; - if (expr.startsWith("import(", i)) { - int j = i + "import(".length(); - int depth = 1; - boolean inString = false; - char stringChar = 0; - while (j < len && depth > 0) { - char c = expr.charAt(j); - if (inString) { - if (c == stringChar && (j == 0 || expr.charAt(j - 1) != '\\')) { - inString = false; - } - } else { - if (c == '\'' || c == '"') { - inString = true; - stringChar = c; - } else if (c == '(') { - depth++; - } else if (c == ')') { - depth--; - } - } - j++; - } - // j is positioned just after ')' - i = j; - // Skip whitespace - while (i < len && Character.isWhitespace(expr.charAt(i))) { - i++; - } - // Optional dot before the referenced type - if (i < len && expr.charAt(i) == '.') { + // Read the base type name (can include dots for qualified names) + StringBuilder baseNameBuilder = new StringBuilder(); + while (i < len) { + char c = expr.charAt(i); + if (Character.isJavaIdentifierPart(c) || c == '.') { + baseNameBuilder.append(c); i++; - while (i < len && Character.isWhitespace(expr.charAt(i))) { - i++; - } - } - - StringBuilder nameBuilder = new StringBuilder(); - while (i < len) { - char c = expr.charAt(i); - if (Character.isJavaIdentifierPart(c) || c == '.') { - nameBuilder.append(c); - i++; - } else { - break; - } - } - baseName = nameBuilder.toString().trim(); - } else { - // Read the base type name (can include dots for qualified names) - StringBuilder baseNameBuilder = new StringBuilder(); - while (i < len) { - char c = expr.charAt(i); - if (Character.isJavaIdentifierPart(c) || c == '.') { - baseNameBuilder.append(c); - i++; - } else { - break; - } + } else { + break; } - baseName = baseNameBuilder.toString().trim(); } + String baseName = baseNameBuilder.toString().trim(); if (baseName.isEmpty()) { return null; @@ -277,7 +181,7 @@ private static ParseResult parseTypeWithGenerics(String expr, int startIndex) { } } - ParsedType type = new ParsedType(baseName, typeArgs, false, 0, false, expr.substring(startIndex, i)); + ParsedType type = new ParsedType(baseName, typeArgs, expr.substring(startIndex, i)); return new ParseResult(type, i); } @@ -363,7 +267,7 @@ private static ParseResult parseTypeArgument(String expr, int startIndex) { } } // Plain wildcard, treat as Object - return new ParseResult(new ParsedType("Object", null, false, 0, false, "?"), i); + return new ParseResult(new ParsedType("Object", null, "?"), i); } // Parse regular type with potential generics @@ -380,33 +284,22 @@ private static ParseResult parseTypeArgument(String expr, int startIndex) { i++; } - // Check for array suffix - int arrayDims = 0; + // Consume array suffixes / nullable suffix so the caller can correctly find delimiters. while (i + 1 < len && expr.charAt(i) == '[' && expr.charAt(i + 1) == ']') { - arrayDims++; i += 2; - // Skip whitespace while (i < len && Character.isWhitespace(expr.charAt(i))) { i++; } } - - // Check for nullable suffix - boolean isNullable = false; if (i < len && expr.charAt(i) == '?') { - isNullable = true; i++; } - - // If we have array dims or nullable, wrap the base type - if (arrayDims > 0 || isNullable) { - return new ParseResult( - new ParsedType(baseType.baseName, baseType.typeArgs, arrayDims > 0, arrayDims, isNullable, - expr.substring(startIndex, i)), - i - ); + + // Preserve the full raw substring (including suffixes) for higher-level resolvers. + if (i != typeResult.endIndex) { + return new ParseResult(new ParsedType(baseType.baseName, baseType.typeArgs, expr.substring(startIndex, i)), i); } - + return new ParseResult(baseType, i); } 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 7ba64041e..56f74dffd 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 @@ -324,54 +324,68 @@ public TypeInfo resolveJSType(String jsTypeName) { return TypeInfo.fromPrimitive("void"); } - // Use the generic type parser for full handling - GenericTypeParser.ParsedType parsed = GenericTypeParser.parse(jsTypeName); - if (parsed == null) { - return TypeInfo.unresolved(jsTypeName, jsTypeName); + // Normalize import() references first. + String normalized = TypeStringNormalizer.stripImportTypeSyntax(jsTypeName); + if (normalized == null || normalized.isEmpty()) { + return TypeInfo.fromPrimitive("void"); } - - // Resolve the parsed type tree into a TypeInfo tree - return resolveFromParsedType(parsed); - } - - /** - * Resolve a ParsedType tree into a TypeInfo, preserving generic arguments. - * This is the core resolution method that handles nested generics. - */ - private TypeInfo resolveFromParsedType(GenericTypeParser.ParsedType parsed) { - if (parsed == null) { - return null; + + // Unions: pick a single "best" branch for resolution (prefer non-nullish). + normalized = TypeStringNormalizer.pickPreferredUnionBranch(normalized); + + // Nullable: treat Foo? as Foo | null (for resolution purposes this means "resolve Foo"). + normalized = TypeStringNormalizer.stripNullableSuffix(normalized); + + // Arrays: handled here, not in the generics structural parser. + TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(normalized); + String baseExpr = arraySplit.base; + int arrayDims = arraySplit.dimensions; + + if (baseExpr == null || baseExpr.isEmpty()) { + return TypeInfo.unresolved(jsTypeName, jsTypeName); } - - String baseName = parsed.baseName; - - // Handle "Java." prefix - convert to actual Java type - if (baseName.startsWith("Java.")) { - baseName = baseName.substring(5); // Remove "Java." + + TypeInfo resolved = null; + // Fast path: no generics - skip expensive parsing + if (!baseExpr.contains("<")) { + resolved = resolveBaseType(baseExpr); + } else { + // Slow path: parse and resolve into a TypeInfo, preserving generic arguments. + GenericTypeParser.ParsedType parsed = GenericTypeParser.parse(baseExpr); + if (parsed != null) { + // Resolve the base type (without generics) + TypeInfo baseType = resolveBaseType(parsed.baseName); + + // If we have type arguments, resolve them (allowing each arg to have its own arrays/unions/etc). + if (parsed.hasTypeArgs() && baseType != null && baseType.isResolved()) { + List resolvedArgs = new ArrayList<>(); + for (GenericTypeParser.ParsedType argParsed : parsed.typeArgs) { + if (argParsed == null) { + resolvedArgs.add(TypeInfo.ANY); + continue; + } + TypeInfo argType = resolveJSType(argParsed.rawString); + resolvedArgs.add(argType != null ? argType : TypeInfo.unresolved(argParsed.baseName, + argParsed.baseName)); + } + if (!resolvedArgs.isEmpty()) { + baseType = baseType.parameterize(resolvedArgs); + } + } + resolved = baseType != null ? baseType : TypeInfo.unresolved(parsed.baseName, parsed.rawString); + } else + resolved = resolveBaseType(baseExpr); } - - // Resolve the base type (without generics) - TypeInfo baseType = resolveBaseType(baseName); - - // If we have type arguments, recursively resolve them and create parameterized type - if (parsed.hasTypeArgs() && baseType != null && baseType.isResolved()) { - List resolvedArgs = new ArrayList<>(); - for (GenericTypeParser.ParsedType argParsed : parsed.typeArgs) { - TypeInfo argType = resolveFromParsedType(argParsed); - resolvedArgs.add(argType != null ? argType : TypeInfo.unresolved(argParsed.baseName, argParsed.baseName)); - } - baseType = baseType.parameterize(resolvedArgs); + + if (resolved == null) { + resolved = TypeInfo.unresolved(baseExpr, jsTypeName); } - - // Handle array - if (parsed.isArray && baseType != null) { - for (int i = 0; i < parsed.arrayDimensions; i++) { - baseType = TypeInfo.arrayOf(baseType); - } + + for (int i = 0; i < arrayDims; i++) { + resolved = TypeInfo.arrayOf(resolved); } - - // Return the resolved (possibly parameterized) type - return baseType != null ? baseType : TypeInfo.unresolved(parsed.baseName, parsed.rawString); + + return resolved; } /** @@ -383,6 +397,11 @@ private TypeInfo resolveBaseType(String baseName) { return null; } + // Handle "Java." prefix - convert to actual Java type + if (baseName.startsWith("Java.")) { + baseName = baseName.substring(5); + } + // Check synthetic types (Nashorn built-ins, custom types, etc.) if (isSyntheticType(baseName)) { SyntheticType syntheticType = getSyntheticType(baseName); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeStringNormalizer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeStringNormalizer.java new file mode 100644 index 000000000..fd464f6d3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeStringNormalizer.java @@ -0,0 +1,221 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import java.util.ArrayList; +import java.util.List; + +/** + * Shared utilities for cleaning and normalizing type expressions. + * + * This is intentionally a small, dependency-light helper that both the runtime resolver + * and the .d.ts parser can use. + */ +public final class TypeStringNormalizer { + + private TypeStringNormalizer() {} + + public static final class ArraySplit { + public final String base; + public final int dimensions; + + public ArraySplit(String base, int dimensions) { + this.base = base; + this.dimensions = dimensions; + } + } + + /** + * Strip TypeScript import() type references anywhere in the string. + * + * Examples: + * - import('./data/IAction').IAction -> IAction + * - Java.java.util.function.Consumer -> Java.java.util.function.Consumer + */ + public static String stripImportTypeSyntax(String type) { + if (type == null) { + return null; + } + + String out = type.trim(); + int importIdx; + while ((importIdx = out.indexOf("import(")) >= 0) { + int i = importIdx + "import(".length(); + int depth = 1; + boolean inString = false; + char stringChar = 0; + + while (i < out.length() && depth > 0) { + char c = out.charAt(i); + if (inString) { + if (c == stringChar && (i == 0 || out.charAt(i - 1) != '\\')) { + inString = false; + } + } else { + if (c == '\'' || c == '"') { + inString = true; + stringChar = c; + } else if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + } + } + i++; + } + + // i is positioned just after the matching ')', if found. + if (depth != 0) { + break; // Unbalanced import( ... ) - stop trying to clean. + } + + // If immediately followed by ".", remove the "import(...)." prefix. + if (i < out.length() && out.charAt(i) == '.') { + int removeEnd = i + 1; + out = out.substring(0, importIdx) + out.substring(removeEnd); + } else { + // Remove just the import(...) portion. + out = out.substring(0, importIdx) + out.substring(i); + } + } + + return out; + } + + /** + * Split a trailing TypeScript nullable suffix: Foo? -> Foo. + */ + public static String stripNullableSuffix(String type) { + if (type == null) { + return null; + } + String out = type.trim(); + if (out.endsWith("?")) { + return out.substring(0, out.length() - 1).trim(); + } + return out; + } + + /** + * Split trailing array suffixes: Foo[][] -> base=Foo, dimensions=2. + */ + public static ArraySplit splitArraySuffixes(String type) { + if (type == null) { + return new ArraySplit(null, 0); + } + String out = type.trim(); + int dims = 0; + while (out.endsWith("[]")) { + dims++; + out = out.substring(0, out.length() - 2).trim(); + } + return new ArraySplit(out, dims); + } + + /** + * Split a top-level union (depth-aware) into branches. + */ + public static List splitTopLevelUnion(String type) { + List parts = new ArrayList<>(); + if (type == null) { + return parts; + } + + String expr = type.trim(); + if (expr.isEmpty()) { + parts.add(""); + return parts; + } + + int depth = 0; + boolean inString = false; + char stringChar = 0; + + int start = 0; + for (int i = 0; i < expr.length(); i++) { + char c = expr.charAt(i); + + if (inString) { + if (c == stringChar && (i == 0 || expr.charAt(i - 1) != '\\')) { + inString = false; + } + continue; + } + + if (c == '\'' || c == '"' || c == '`') { + inString = true; + stringChar = c; + continue; + } + + if (c == '<' || c == '(' || c == '[' || c == '{') { + depth++; + continue; + } + if (c == '>' || c == ')' || c == ']' || c == '}') { + if (depth > 0) { + depth--; + } + continue; + } + + if (c == '|' && depth == 0) { + String part = expr.substring(start, i).trim(); + if (!part.isEmpty()) { + parts.add(part); + } + start = i + 1; + } + } + + String tail = expr.substring(start).trim(); + if (!tail.isEmpty()) { + parts.add(tail); + } + + if (parts.isEmpty()) { + parts.add(expr); + } + return parts; + } + + /** + * Choose a single branch to represent a union type for resolution. + * + * Policy: + * - Prefer the first non-nullish branch (not null/undefined/void). + * - Otherwise, fall back to the first branch. + */ + public static String pickPreferredUnionBranch(String type) { + if (type == null) { + return null; + } + + List parts = splitTopLevelUnion(type); + if (parts.size() <= 1) { + return type.trim(); + } + + for (String part : parts) { + if (!isNullishBranch(part)) { + return part.trim(); + } + } + + return parts.get(0).trim(); + } + + private static boolean isNullishBranch(String part) { + if (part == null) { + return false; + } + + String s = part.trim(); + if (s.isEmpty()) { + return false; + } + + // Strip generics so "null<...>" doesn't behave weirdly. + s = GenericTypeParser.stripGenerics(s); + s = s.trim().toLowerCase(); + return "null".equals(s) || "undefined".equals(s) || "void".equals(s); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java index 3eed427fd..514174743 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeSubstitutor.java @@ -93,7 +93,7 @@ public static TypeInfo substitute(TypeInfo type, Map bindings) String typeName = type.getSimpleName(); // Check if this type is itself a type variable - if (!type.isResolved() || isTypeVariable(typeName)) { + if (!type.isResolved()) { TypeInfo substitution = bindings.get(typeName); if (substitution != null) { return substitution; @@ -122,11 +122,16 @@ public static TypeInfo substitute(TypeInfo type, Map bindings) // Handle array types - substitute the element type if (type.getSimpleName().endsWith("[]")) { - // Extract element type and substitute - String elementTypeName = typeName.substring(0, typeName.length() - 2); - TypeInfo elementSubstitution = bindings.get(elementTypeName); + TypeStringNormalizer.ArraySplit split = TypeStringNormalizer.splitArraySuffixes(typeName); + String elementTypeName = split.base; + int dims = split.dimensions; + TypeInfo elementSubstitution = elementTypeName != null ? bindings.get(elementTypeName) : null; if (elementSubstitution != null) { - return TypeInfo.arrayOf(elementSubstitution); + TypeInfo result = elementSubstitution; + for (int i = 0; i < dims; i++) { + result = TypeInfo.arrayOf(result); + } + return result; } } @@ -148,11 +153,13 @@ public static TypeInfo substituteString(String typeString, Map } // First check if the whole string is a type variable - String trimmed = typeString.trim(); - - // Handle array suffix - boolean isArray = trimmed.endsWith("[]"); - String baseName = isArray ? trimmed.substring(0, trimmed.length() - 2).trim() : trimmed; + String trimmed = TypeStringNormalizer.stripImportTypeSyntax(typeString); + trimmed = TypeStringNormalizer.pickPreferredUnionBranch(trimmed); + trimmed = TypeStringNormalizer.stripNullableSuffix(trimmed); + + TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(trimmed); + String baseName = arraySplit.base; + int arrayDims = arraySplit.dimensions; // Strip generic args to check the base name String bareBaseName = GenericTypeParser.stripGenerics(baseName); @@ -162,7 +169,7 @@ public static TypeInfo substituteString(String typeString, Map if (directSubstitution != null) { // It's a type variable, return the substitution TypeInfo result = directSubstitution; - if (isArray) { + for (int i = 0; i < arrayDims; i++) { result = TypeInfo.arrayOf(result); } return result; @@ -173,20 +180,6 @@ public static TypeInfo substituteString(String typeString, Map return substitute(resolved, bindings); } - /** - * Check if a type name looks like a type variable (single uppercase letter or short uppercase name). - */ - private static boolean isTypeVariable(String name) { - if (name == null || name.isEmpty()) { - return false; - } - // Common type variable patterns: T, E, K, V, R, U, etc. - // Also handles longer ones like KEY, VALUE but those are rare - return name.length() <= 3 && - Character.isUpperCase(name.charAt(0)) && - name.chars().allMatch(c -> Character.isUpperCase(c) || Character.isDigit(c)); - } - /** * Get the return type for a method on a parameterized receiver, with type variable substitution. * @@ -198,25 +191,7 @@ private static boolean isTypeVariable(String name) { */ public static TypeInfo getSubstitutedReturnType(TypeInfo rawReturnType, String rawReturnTypeString, TypeInfo receiverType, TypeResolver resolver) { - Map bindings = createBindingsFromReceiver(receiverType); - if (bindings.isEmpty()) { - return rawReturnType; - } - - // Try substituting the resolved type first - TypeInfo substituted = substitute(rawReturnType, bindings); - if (substituted != rawReturnType && substituted.isResolved()) { - return substituted; - } - - // If still not resolved, try substituting from the string - if (rawReturnTypeString != null && !rawReturnTypeString.isEmpty()) { - TypeInfo fromString = substituteString(rawReturnTypeString, bindings, resolver); - if (fromString != null && fromString.isResolved()) { - return fromString; - } - } - - return substituted; + GenericContext ctx = GenericContext.forReceiver(receiverType); + return ctx.substituteType(rawReturnType, rawReturnTypeString, resolver); } } From 969d82dff892d4a5fc275907a98e749e02c48c2c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 28 Jan 2026 04:55:32 +0200 Subject: [PATCH 314/337] Fixed visual hover info bugs for generics --- .../client/gui/util/script/interpreter/ScriptDocument.java | 5 ++++- .../gui/util/script/interpreter/hover/TokenHoverInfo.java | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) 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 d3c9c19e3..15104c56f 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 @@ -1295,11 +1295,14 @@ private void parseLocalVariables() { initStart = bodyStart + m.start(3); int searchPos = bodyStart + m.end(3); int depth = 0; + int angleDepth = 0; while (searchPos < text.length()) { char c = text.charAt(searchPos); if (c == '(' || c == '[' || c == '{') depth++; else if (c == ')' || c == ']' || c == '}') depth--; - else if ((c == ';' || c == ',') && depth == 0) { + else if (c == '<') angleDepth++; + else if (c == '>') angleDepth--; + else if ((c == ';' || c == ',') && depth == 0 && angleDepth == 0) { initEnd = searchPos; break; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java index 8c0669dbe..137c4db02 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/hover/TokenHoverInfo.java @@ -1496,12 +1496,14 @@ private boolean shouldAddSpace(String lastToken, String currentToken) { // Never add space before these closing/trailing characters if (firstChar == '(' || firstChar == '[' || firstChar == '{' || firstChar == '.' || firstChar == ',' || firstChar == ';' || + firstChar == '>' || // No space before closing angle bracket (firstChar == ':' && lastToken.equals("?"))) { return false; } // Never add space after these opening/leading characters - if (lastChar == '(' || lastChar == '[' || lastChar == '{' || lastChar == '.') { + if (lastChar == '(' || lastChar == '[' || lastChar == '{' || + lastChar == '.' || lastChar == '<') { // No space after opening angle bracket return false; } From 6c5a269966aa5d47d976ef91400aae5aff0734e5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 28 Jan 2026 06:06:01 +0200 Subject: [PATCH 315/337] Fixed Autocomplete suggestions not working when whitespaces exist between it and receiver like "player. get <- doesnt work due to WS" --- .../autocomplete/AutocompleteManager.java | 151 +++++++++++++----- 1 file changed, 114 insertions(+), 37 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index ca2cde68c..8da4f95c4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -1,11 +1,13 @@ package noppes.npcs.client.gui.util.script.autocomplete; +import noppes.npcs.api.handler.data.IAction; import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import org.lwjgl.input.Mouse; import java.util.List; +import java.util.function.Consumer; import java.util.regex.Pattern; /** @@ -642,6 +644,15 @@ private String findCurrentWord(String text, int cursorPos) { /** * Find the receiver expression before a dot. + * Handles multiline chains and whitespace intelligently. + * + * Stops at statement boundaries: + * - Semicolons (Java) + * - Blank lines or newlines not part of chain continuation + * + * Continues through: + * - Dots at start/end of lines (chain continuation pattern) + * - Whitespace within expressions */ private String findReceiverExpression(String text, int dotPos) { if (text == null || dotPos <= 0) return ""; @@ -650,67 +661,133 @@ private String findReceiverExpression(String text, int dotPos) { int start = end; int parenDepth = 0; int bracketDepth = 0; + boolean sawNewline = false; // Track if we've seen a newline // Walk backwards to find the start of the expression while (start > 0) { char c = text.charAt(start - 1); - if (c == ')') { - parenDepth++; - start--; - } else if (c == '(') { - if (parenDepth > 0) { - parenDepth--; + // Inside parens/brackets: allow everything including newlines + if (parenDepth > 0 || bracketDepth > 0) { + if (c == ')') parenDepth++; + else if (c == '(') parenDepth--; + else if (c == ']') bracketDepth++; + else if (c == '[') bracketDepth--; + else if (c == '"' || c == '\'') { + // Skip string literals start--; - } else { - break; + char quote = c; + while (start > 0) { + char strChar = text.charAt(start - 1); + start--; + if (strChar == quote && (start == 0 || text.charAt(start - 1) != '\\')) { + break; + } + } + continue; } - } else if (c == ']') { - bracketDepth++; start--; - } else if (c == '[') { - if (bracketDepth > 0) { - bracketDepth--; + continue; + } + + // Not inside depth - handle based on character + if (c == ')' || c == ']') { + // Start tracking depth + if (c == ')') parenDepth++; + else bracketDepth++; + start--; + } else if (c == '(' || c == '[') { + // Hit opening without matching closing - stop + break; + } else if (c == '.') { + // Dot is a chain operator - continue + start--; + sawNewline = false; // Reset newline tracking after seeing dot + } else if (Character.isJavaIdentifierPart(c)) { + // Part of identifier + start--; + } else if (c == ';') { + // Semicolon always terminates statement + break; + } else if (c == '\n') { + // Newline - check if it's part of a chain continuation + if (sawNewline) { + // Second newline = blank line = statement boundary + break; + } + + // Check if this is a chain continuation pattern + boolean isContinuation = false; + + // Look forward: does the next line start with a dot? + int forwardPos = start; + while (forwardPos < end && Character.isWhitespace(text.charAt(forwardPos))) { + forwardPos++; + } + if (forwardPos < end && text.charAt(forwardPos) == '.') { + isContinuation = true; + } + + // Look backward: did the previous line end with a dot? + if (!isContinuation) { + int backPos = start - 2; // skip the \n we're at + while (backPos >= 0 && (text.charAt(backPos) == ' ' || text.charAt(backPos) == '\t' || text.charAt(backPos) == '\r')) { + backPos--; + } + if (backPos >= 0 && text.charAt(backPos) == '.') { + isContinuation = true; + } + } + + if (isContinuation) { + // Part of chain - continue but mark that we saw a newline + sawNewline = true; start--; } else { - break; + // Not a continuation - check if previous significant char suggests chain + int backPos = start - 2; + while (backPos >= 0 && Character.isWhitespace(text.charAt(backPos))) { + backPos--; + } + + if (backPos >= 0) { + char prevChar = text.charAt(backPos); + // Only continue if previous was dot, paren, or bracket (active chain) + if (prevChar == '.' || prevChar == ')' || prevChar == ']' || prevChar == '>') { + sawNewline = true; + start--; + } else { + // Previous char suggests end of statement + break; + } + } else { + break; + } } + } else if (c == ' ' || c == '\t' || c == '\r') { + // Other whitespace - skip but don't break yet + start--; } else if (c == '"' || c == '\'') { - // Skip over string literals + // String literal - skip it char quote = c; start--; while (start > 0) { char strChar = text.charAt(start - 1); - if (strChar == quote && (start < 2 || text.charAt(start - 2) != '\\')) { - // Found unescaped closing quote - start--; + start--; + if (strChar == quote && (start == 0 || text.charAt(start - 1) != '\\')) { break; } - start--; - } - } else if (Character.isJavaIdentifierPart(c) || c == '.') { - start--; - } else if (Character.isWhitespace(c)) { - // Skip whitespace within expression (e.g., after method call) - if (parenDepth > 0 || bracketDepth > 0) { - start--; - } else { - break; } } else { - // Within parentheses or brackets, allow any character (commas, operators, etc.) - if (parenDepth > 0 || bracketDepth > 0) { - start--; - } else { - break; - } + // Any other character (operator, etc.) - stop + break; } } - String expr = text.substring(start, end).trim(); + String expr = text.substring(start, end); - // Clean up the expression - expr = expr.replaceAll("\\s+", ""); + // Normalize whitespace: replace multiple spaces/newlines with single space + expr = expr.replaceAll("\\s+", " ").trim(); return expr; } From 9e04a1184cce1a412eafabe3478ec2822088eb8d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 28 Jan 2026 06:59:58 +0200 Subject: [PATCH 316/337] Lambda impl Phase 1: Track inner callables as nested scopes so parameters and locals resolve correctly inside the callable body, --- .../interpreter/InnerCallableScope.java | 99 +++++ .../script/interpreter/ScriptDocument.java | 350 ++++++++++++++++++ .../script/interpreter/type/TypeInfo.java | 156 ++++++++ 3 files changed, 605 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java new file mode 100644 index 000000000..5e05028d1 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/InnerCallableScope.java @@ -0,0 +1,99 @@ +package noppes.npcs.client.gui.util.script.interpreter; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; + +/** + * Represents an inner callable scope (lambda or JS function expression). + * These are NOT added to the methods list - they're resolution-only scopes. + */ +public class InnerCallableScope { + + public enum Kind { + JAVA_LAMBDA, + JS_FUNCTION_EXPR + } + + private final Kind kind; + private final int headerStart; // Start of "(params) ->" or "function(params)" + private final int headerEnd; // End of header (position of "->" for lambda, ")" for JS func) + private final int bodyStart; // Start of body (expression start or '{') + private final int bodyEnd; // End of body (expression end or '}') + private final List parameters = new ArrayList<>(); + private final Map locals = new HashMap<>(); + private InnerCallableScope parentScope; // Enclosing inner scope (for nested lambdas) + private TypeInfo expectedType; // The expected functional interface type (set during resolution) + + public InnerCallableScope(Kind kind, int headerStart, int headerEnd, int bodyStart, int bodyEnd) { + this.kind = kind; + this.headerStart = headerStart; + this.headerEnd = headerEnd; + this.bodyStart = bodyStart; + this.bodyEnd = bodyEnd; + } + + // Getters + public Kind getKind() { return kind; } + public int getHeaderStart() { return headerStart; } + public int getHeaderEnd() { return headerEnd; } + public int getBodyStart() { return bodyStart; } + public int getBodyEnd() { return bodyEnd; } + public List getParameters() { return parameters; } + public Map getLocals() { return locals; } + public InnerCallableScope getParentScope() { return parentScope; } + public TypeInfo getExpectedType() { return expectedType; } + + // Setters + public void setParentScope(InnerCallableScope parent) { this.parentScope = parent; } + public void setExpectedType(TypeInfo type) { this.expectedType = type; } + + public void addParameter(FieldInfo param) { + parameters.add(param); + } + + public void addLocal(String name, FieldInfo local) { + locals.put(name, local); + } + + public FieldInfo getParameter(String name) { + for (FieldInfo param : parameters) { + if (param.getName().equals(name)) { + return param; + } + } + return null; + } + + public boolean hasParameter(String name) { + return getParameter(name) != null; + } + + /** + * Check if a position is inside this scope's body. + */ + public boolean containsPosition(int position) { + return position >= bodyStart && position < bodyEnd; + } + + /** + * Check if a position is inside this scope's header (parameter list). + */ + public boolean containsHeaderPosition(int position) { + return position >= headerStart && position < headerEnd; + } + + /** + * Get the full range (header + body). + */ + public int getFullStart() { + return headerStart; + } + + public int getFullEnd() { + return bodyEnd; + } +} 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 15104c56f..a12bfff5a 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 @@ -106,6 +106,9 @@ public class ScriptDocument { // Local variables per method (methodStartOffset -> {varName -> FieldInfo}) private final Map> methodLocals = new HashMap<>(); + // Inner callable scopes (lambdas, JS function expressions) - NOT in methods list + private final List innerScopes = new ArrayList<>(); + // Method calls - stores all parsed method call information private final List methodCalls = new ArrayList<>(); // Field accesses - stores all parsed field access information @@ -331,6 +334,7 @@ public void formatCodeText() { excludedRanges.clear(); methodLocals.clear(); scriptTypes.clear(); + innerScopes.clear(); methodCalls.clear(); externalFieldAssignments.clear(); declarationErrors.clear(); @@ -653,6 +657,9 @@ private void parseStructure() { // Parse methods/functions - UNIFIED for both languages parseMethodDeclarations(); + // Parse inner callable scopes (lambdas, JS function expressions) + parseInnerCallableScopes(); + // Parse local variables inside methods/functions - UNIFIED for both languages parseLocalVariables(); @@ -1532,6 +1539,322 @@ private TypeInfo inferChainType(String firstIdent, int identStart, int dotPositi return currentType; } + /** + * Parse inner callable scopes (lambdas and JS function expressions). + * These are NOT added to the methods list - they're resolution-only scopes. + */ + private void parseInnerCallableScopes() { + // For Java: detect (params) -> expr and (params) -> { ... } and param -> expr + // For JS: detect function(params) { ... } that are expressions (not declarations) + + if (isJavaScript()) { + parseJSFunctionExpressions(); + } else { + parseJavaLambdas(); + } + + // Sort by header start and set up parent relationships + innerScopes.sort((a, b) -> Integer.compare(a.getHeaderStart(), b.getHeaderStart())); + setupScopeParents(); + } + + private void parseJavaLambdas() { + // Pattern: (params) -> or identifier -> + // Look for -> that's not inside strings/comments + String arrowPattern = "->"; + int pos = 0; + while ((pos = text.indexOf(arrowPattern, pos)) >= 0) { + if (isExcluded(pos)) { + pos += 2; + continue; + } + + // Found a potential lambda arrow + int arrowPos = pos; + + // Look backwards to find the parameter list + int headerStart = findLambdaHeaderStart(arrowPos); + if (headerStart < 0) { + pos += 2; + continue; + } + + // Look forward to find the body end + int bodyStart = arrowPos + 2; + // Skip whitespace after -> + while (bodyStart < text.length() && Character.isWhitespace(text.charAt(bodyStart))) { + bodyStart++; + } + + if (bodyStart >= text.length()) { + pos += 2; + continue; + } + + int bodyEnd; + if (text.charAt(bodyStart) == '{') { + // Block lambda + bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) bodyEnd = text.length(); + else bodyEnd++; // Include the closing brace + } else { + // Expression lambda - find end of expression + bodyEnd = findLambdaExpressionEnd(bodyStart); + } + + InnerCallableScope scope = new InnerCallableScope( + InnerCallableScope.Kind.JAVA_LAMBDA, + headerStart, + arrowPos + 2, + bodyStart, + bodyEnd + ); + + // Parse parameters (will be typed later during resolution) + parseLambdaParameters(scope, headerStart, arrowPos); + + innerScopes.add(scope); + pos = bodyEnd; + } + } + + private int findLambdaHeaderStart(int arrowPos) { + // Work backwards from arrow to find start of parameter list + int pos = arrowPos - 1; + + // Skip whitespace + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) { + pos--; + } + + if (pos < 0) return -1; + + if (text.charAt(pos) == ')') { + // Parenthesized params: find matching ( + int depth = 1; + pos--; + while (pos >= 0 && depth > 0) { + if (isExcluded(pos)) { + pos--; + continue; + } + char c = text.charAt(pos); + if (c == ')') depth++; + else if (c == '(') depth--; + pos--; + } + return pos + 1; // Position of '(' + } else if (Character.isJavaIdentifierPart(text.charAt(pos))) { + // Single identifier param (no parens): identifier -> + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) { + pos--; + } + return pos + 1; + } + + return -1; + } + + private int findLambdaExpressionEnd(int start) { + // Find end of expression lambda body + // Ends at: ; , ) ] } (at depth 0) or end of text + int depth = 0; + int angleDepth = 0; + int pos = start; + boolean inString = false; + char stringChar = 0; + + while (pos < text.length()) { + char c = text.charAt(pos); + + // Handle strings + if (!inString && (c == '"' || c == '\'')) { + inString = true; + stringChar = c; + pos++; + continue; + } + if (inString) { + if (c == stringChar && (pos == 0 || text.charAt(pos - 1) != '\\')) { + inString = false; + } + pos++; + continue; + } + + // Track nesting + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') { + if (depth == 0) return pos; + depth--; + } + else if (c == '<') angleDepth++; + else if (c == '>') angleDepth = Math.max(0, angleDepth - 1); + else if ((c == ';' || c == ',') && depth == 0 && angleDepth == 0) { + return pos; + } + + pos++; + } + + return pos; + } + + private void parseLambdaParameters(InnerCallableScope scope, int headerStart, int arrowPos) { + String headerText = text.substring(headerStart, arrowPos).trim(); + + if (headerText.startsWith("(") && headerText.endsWith(")")) { + // Parenthesized: (a, b) or (Type a, Type b) + String paramsText = headerText.substring(1, headerText.length() - 1).trim(); + if (!paramsText.isEmpty()) { + String[] params = paramsText.split(","); + int offset = headerStart + 1; // After '(' + for (String param : params) { + param = param.trim(); + if (param.isEmpty()) continue; + + // Find position in original text + int paramOffset = text.indexOf(param, offset); + if (paramOffset < 0) paramOffset = offset; + + String[] parts = param.split("\\s+"); + String paramName; + TypeInfo paramType = null; + + if (parts.length >= 2) { + // Typed: Type name + paramName = parts[parts.length - 1]; + String typeName = parts[parts.length - 2]; + paramType = resolveType(typeName); + } else { + // Untyped: just name (type will be inferred from expected FI) + paramName = parts[0]; + } + + int nameOffset = text.indexOf(paramName, paramOffset); + if (nameOffset < 0) nameOffset = paramOffset; + + FieldInfo paramInfo = FieldInfo.parameter(paramName, paramType, nameOffset, null); + scope.addParameter(paramInfo); + + offset = paramOffset + param.length(); + } + } + } else { + // Single identifier: x -> + String paramName = headerText; + int nameOffset = text.indexOf(paramName, headerStart); + if (nameOffset < 0) nameOffset = headerStart; + + FieldInfo paramInfo = FieldInfo.parameter(paramName, null, nameOffset, null); + scope.addParameter(paramInfo); + } + } + + private void parseJSFunctionExpressions() { + // Pattern: function(params) { ... } or function name(params) { ... } + // But NOT function declarations at top level (those are already in methods) + Pattern funcExprPattern = Pattern.compile("function\\s*(?:\\w+)?\\s*\\(([^)]*)\\)\\s*\\{"); + Matcher m = funcExprPattern.matcher(text); + + while (m.find()) { + int start = m.start(); + if (isExcluded(start)) continue; + + // Check if this is a function expression (not a declaration) + // A function declaration is at statement level; expression is inside parens, after =, etc. + if (isFunctionDeclaration(start)) continue; + + int headerStart = start; + int headerEnd = m.end() - 1; // Position of '{' + int bodyStart = m.end() - 1; + int bodyEnd = findMatchingBrace(bodyStart); + if (bodyEnd < 0) bodyEnd = text.length(); + else bodyEnd++; // Include closing brace + + InnerCallableScope scope = new InnerCallableScope( + InnerCallableScope.Kind.JS_FUNCTION_EXPR, + headerStart, + headerEnd, + bodyStart, + bodyEnd + ); + + // Parse parameters + String paramsText = m.group(1).trim(); + if (!paramsText.isEmpty()) { + String[] params = paramsText.split(","); + int offset = m.start(1); + for (String param : params) { + param = param.trim(); + if (param.isEmpty()) continue; + + int nameOffset = text.indexOf(param, offset); + if (nameOffset < 0) nameOffset = offset; + + // JS params don't have types by default; will infer from expected FI + FieldInfo paramInfo = FieldInfo.parameter(param, TypeInfo.ANY, nameOffset, null); + scope.addParameter(paramInfo); + + offset = nameOffset + param.length(); + } + } + + innerScopes.add(scope); + } + } + + private boolean isFunctionDeclaration(int funcStart) { + // A function declaration is a statement, check context: + // - If preceded by = or ( or , or : or [ it's an expression + // - If at line start (possibly with whitespace) or after { or ; it's a declaration + + int pos = funcStart - 1; + while (pos >= 0 && Character.isWhitespace(text.charAt(pos)) && text.charAt(pos) != '\n') { + pos--; + } + + if (pos < 0) return true; // Start of file = declaration + + char prev = text.charAt(pos); + // These characters indicate it's an expression, not a declaration + if (prev == '=' || prev == '(' || prev == ',' || prev == ':' || prev == '[' || prev == '?') { + return false; + } + // After newline + whitespace could be declaration + if (prev == '\n' || prev == '{' || prev == ';' || prev == '}') { + return true; + } + + return true; // Default to declaration to avoid false positives in inner scopes + } + + private void setupScopeParents() { + // For each scope, find its immediate parent (smallest enclosing scope) + for (int i = 0; i < innerScopes.size(); i++) { + InnerCallableScope scope = innerScopes.get(i); + InnerCallableScope parent = null; + + for (int j = 0; j < innerScopes.size(); j++) { + if (i == j) continue; + InnerCallableScope candidate = innerScopes.get(j); + + // Check if candidate contains scope + if (candidate.getBodyStart() < scope.getHeaderStart() && + candidate.getBodyEnd() > scope.getFullEnd()) { + // candidate contains scope + if (parent == null || + (candidate.getBodyStart() > parent.getBodyStart())) { + // candidate is smaller (more immediate) than current parent + parent = candidate; + } + } + } + + scope.setParentScope(parent); + } + } + /** * Parse global fields/variables (outside methods) - UNIFIED for both Java and JavaScript. * Stores results in the shared 'globalFields' map. @@ -5857,6 +6180,29 @@ private MethodInfo findMethodAtPosition(int position) { return null; } + /** + * Find the innermost scope (method or inner callable) at a position. + * Returns null if not inside any scope. + */ + public Object findInnermostScopeAt(int position) { + // First check inner callable scopes (most specific) + InnerCallableScope innermost = null; + for (InnerCallableScope scope : innerScopes) { + if (scope.containsPosition(position)) { + if (innermost == null || scope.getBodyStart() > innermost.getBodyStart()) { + innermost = scope; + } + } + } + + if (innermost != null) { + return innermost; + } + + // Fall back to method scope + return findMethodAtPosition(position); + } + private int findMatchingBrace(int openBraceIndex) { if (openBraceIndex < 0 || openBraceIndex >= text.length()) return -1; @@ -5892,6 +6238,10 @@ public List getMethods() { return Collections.unmodifiableList(methods); } + public List getInnerScopes() { + return Collections.unmodifiableList(innerScopes); + } + public List getScriptTypes() { return scriptTypes.values().stream().collect(Collectors.toList()); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index fd2eaec77..ddfcadb1e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -82,6 +82,10 @@ public enum Kind { // Documentation (script-defined types) private JSDocInfo jsDocInfo; + // SAM (Single Abstract Method) caching for functional interface detection + private MethodInfo cachedSAM; + private boolean samCacheResolved = false; + private TypeInfo(String simpleName, String fullName, String packageName, Kind kind, Class javaClass, boolean resolved, TypeInfo enclosingType) { this(simpleName, fullName, packageName, kind, javaClass, resolved, enclosingType, null, null, null); @@ -937,6 +941,158 @@ public void validate() { // Default: no validation for Java types } + // ==================== Functional Interface (SAM) Detection ==================== + + /** + * Determine whether this type is a functional interface and return its single abstract method (SAM). + * + * For Java reflection types (javaClass != null): + * - Must be an interface + * - Collects all public methods from javaClass.getMethods() + * - Filters out: static methods, default methods, and Object methods + * - If exactly one abstract instance method remains, returns it as a MethodInfo + * + * For script-defined interfaces (ScriptTypeInfo with kind == INTERFACE): + * - Identifies abstract methods (methods without body) + * - If exactly one exists, returns it as a MethodInfo + * + * @return MethodInfo for the single abstract method, or null if not a functional interface + */ + public MethodInfo getSingleAbstractMethod() { + // Return cached result if already resolved + if (samCacheResolved) { + return cachedSAM; + } + + // Mark as resolved to prevent re-computation + samCacheResolved = true; + + try { + // Handle Java reflection types + if (javaClass != null) { + // Only interfaces can be functional interfaces (for simplicity) + if (!javaClass.isInterface()) { + cachedSAM = null; + return null; + } + + // Collect all public methods + java.lang.reflect.Method[] methods = javaClass.getMethods(); + java.lang.reflect.Method singleAbstractMethod = null; + + for (java.lang.reflect.Method method : methods) { + int modifiers = method.getModifiers(); + + // Skip static methods + if (java.lang.reflect.Modifier.isStatic(modifiers)) { + continue; + } + + // Skip default methods (Java 8+) + if (method.isDefault()) { + continue; + } + + // Skip methods inherited from Object + String methodName = method.getName(); + if (isObjectMethod(methodName, method.getParameterCount())) { + continue; + } + + // This is an abstract instance method + if (singleAbstractMethod != null) { + // More than one abstract method - not a functional interface + cachedSAM = null; + return null; + } + + singleAbstractMethod = method; + } + + // If exactly one abstract method found, create MethodInfo with generic substitution + if (singleAbstractMethod != null) { + cachedSAM = MethodInfo.fromReflection(singleAbstractMethod, this); + return cachedSAM; + } + + cachedSAM = null; + return null; + } + + // Handle script-defined interfaces + if (this instanceof ScriptTypeInfo) { + ScriptTypeInfo scriptType = (ScriptTypeInfo) this; + + // Only check interfaces + if (scriptType.getKind() != Kind.INTERFACE) { + cachedSAM = null; + return null; + } + + // Find all abstract methods (methods without body) + // For script-defined interfaces, all declared methods are implicitly abstract + java.util.List allMethods = scriptType.getAllMethodsFlat(); + + if (allMethods.size() == 1) { + // Exactly one method - it's the SAM + cachedSAM = allMethods.get(0); + return cachedSAM; + } else if (allMethods.size() > 1) { + // More than one method - not a functional interface + cachedSAM = null; + return null; + } + + cachedSAM = null; + return null; + } + + // Not a functional interface + cachedSAM = null; + return null; + + } catch (Exception e) { + // Any reflection or security error - treat as not a functional interface + cachedSAM = null; + return null; + } + } + + /** + * Check if this type is a functional interface (has a single abstract method). + * + * @return true if this is a functional interface, false otherwise + */ + public boolean isFunctionalInterface() { + return getSingleAbstractMethod() != null; + } + + /** + * Helper method to check if a method is inherited from java.lang.Object. + * These methods don't count toward the SAM requirement. + * + * @param methodName The name of the method + * @param paramCount The number of parameters + * @return true if this is an Object method + */ + private boolean isObjectMethod(String methodName, int paramCount) { + switch (methodName) { + case "equals": + return paramCount == 1; + case "hashCode": + case "toString": + case "getClass": + case "notify": + case "notifyAll": + return paramCount == 0; + case "wait": + // wait() has overloads with 0, 1, and 2 parameters + return paramCount == 0 || paramCount == 1 || paramCount == 2; + default: + return false; + } + } + // ==================== Type Parameter Methods ==================== /** From 62d87c0c663d0cecb13cb1e31cb4a69ba909da78 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 28 Jan 2026 20:30:41 +0200 Subject: [PATCH 317/337] Added comment/multi-line handling for Autocomplete findReceiverExpression --- .../autocomplete/AutocompleteManager.java | 274 ++++++++++-------- 1 file changed, 156 insertions(+), 118 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index 8da4f95c4..e5d13f286 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -656,141 +656,179 @@ private String findCurrentWord(String text, int cursorPos) { */ private String findReceiverExpression(String text, int dotPos) { if (text == null || dotPos <= 0) return ""; - + if (dotPos >= text.length()) return ""; + int end = dotPos; - int start = end; - int parenDepth = 0; - int bracketDepth = 0; - boolean sawNewline = false; // Track if we've seen a newline - - // Walk backwards to find the start of the expression - while (start > 0) { - char c = text.charAt(start - 1); - - // Inside parens/brackets: allow everything including newlines - if (parenDepth > 0 || bracketDepth > 0) { - if (c == ')') parenDepth++; - else if (c == '(') parenDepth--; - else if (c == ']') bracketDepth++; - else if (c == '[') bracketDepth--; - else if (c == '"' || c == '\'') { - // Skip string literals - start--; - char quote = c; - while (start > 0) { - char strChar = text.charAt(start - 1); - start--; - if (strChar == quote && (start == 0 || text.charAt(start - 1) != '\\')) { - break; - } - } - continue; + int pos = dotPos - 1; + + // Skip whitespace immediately left of dot + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--; + if (pos < 0) return ""; + + // Walk left across a dotted receiver chain. + while (pos >= 0) { + // Skip excluded regions (comments/strings) if the document knows them + if (document != null && document.isExcluded(pos)) { + int jumped = jumpBeforeExcluded(pos); + if (jumped == pos) { + pos--; // Safety fallback + } else { + pos = jumped; } - start--; continue; } - - // Not inside depth - handle based on character - if (c == ')' || c == ']') { - // Start tracking depth - if (c == ')') parenDepth++; - else bracketDepth++; - start--; - } else if (c == '(' || c == '[') { - // Hit opening without matching closing - stop - break; - } else if (c == '.') { - // Dot is a chain operator - continue - start--; - sawNewline = false; // Reset newline tracking after seeing dot - } else if (Character.isJavaIdentifierPart(c)) { - // Part of identifier - start--; - } else if (c == ';') { - // Semicolon always terminates statement - break; - } else if (c == '\n') { - // Newline - check if it's part of a chain continuation - if (sawNewline) { - // Second newline = blank line = statement boundary - break; - } - - // Check if this is a chain continuation pattern - boolean isContinuation = false; - - // Look forward: does the next line start with a dot? - int forwardPos = start; - while (forwardPos < end && Character.isWhitespace(text.charAt(forwardPos))) { - forwardPos++; - } - if (forwardPos < end && text.charAt(forwardPos) == '.') { - isContinuation = true; - } - - // Look backward: did the previous line end with a dot? - if (!isContinuation) { - int backPos = start - 2; // skip the \n we're at - while (backPos >= 0 && (text.charAt(backPos) == ' ' || text.charAt(backPos) == '\t' || text.charAt(backPos) == '\r')) { - backPos--; - } - if (backPos >= 0 && text.charAt(backPos) == '.') { - isContinuation = true; - } - } - - if (isContinuation) { - // Part of chain - continue but mark that we saw a newline - sawNewline = true; - start--; - } else { - // Not a continuation - check if previous significant char suggests chain - int backPos = start - 2; - while (backPos >= 0 && Character.isWhitespace(text.charAt(backPos))) { - backPos--; + + char c = text.charAt(pos); + if (Character.isWhitespace(c)) { + pos--; + continue; + } + + if (c == ')') { + int open = findMatchingBackward(text, pos, '(', ')'); + if (open < 0) break; + pos = open - 1; + continue; + } + + if (c == ']') { + int open = findMatchingBackward(text, pos, '[', ']'); + if (open < 0) break; + pos = open - 1; + continue; + } + + if (Character.isJavaIdentifierPart(c)) { + // Consume identifier + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) pos--; + + // If preceded by a dot (allowing whitespace/newlines between), keep going. + int checkPos = pos; + while (checkPos >= 0) { + char ch = text.charAt(checkPos); + if (Character.isWhitespace(ch)) { + checkPos--; + continue; } - - if (backPos >= 0) { - char prevChar = text.charAt(backPos); - // Only continue if previous was dot, paren, or bracket (active chain) - if (prevChar == '.' || prevChar == ')' || prevChar == ']' || prevChar == '>') { - sawNewline = true; - start--; + // Critical for cases like: "). // comment\n setData(...)" + // The last non-whitespace char before the newline is inside an excluded range, + // so we need to skip excluded ranges when searching for the dot. + if (document != null && document.isExcluded(checkPos)) { + int jumped = jumpBeforeExcluded(checkPos); + if (jumped == checkPos) { + checkPos--; // Safety fallback } else { - // Previous char suggests end of statement - break; + checkPos = jumped; } - } else { - break; + continue; } + break; } - } else if (c == ' ' || c == '\t' || c == '\r') { - // Other whitespace - skip but don't break yet - start--; - } else if (c == '"' || c == '\'') { - // String literal - skip it - char quote = c; - start--; - while (start > 0) { - char strChar = text.charAt(start - 1); - start--; - if (strChar == quote && (start == 0 || text.charAt(start - 1) != '\\')) { - break; - } + if (checkPos >= 0 && text.charAt(checkPos) == '.') { + pos = checkPos - 1; + continue; } - } else { - // Any other character (operator, etc.) - stop break; } + + // Anything else terminates the receiver + break; } - + + int start = pos + 1; + if (start < 0) start = 0; + if (end > text.length()) end = text.length(); + if (start >= end) return ""; + String expr = text.substring(start, end); - - // Normalize whitespace: replace multiple spaces/newlines with single space + + // Mask comment ranges so comment words don't get parsed as identifiers/segments. + // Keep string literal ranges intact so argument type resolution still works. + if (document != null) { + char[] chars = expr.toCharArray(); + for (int[] range : document.getExcludedRanges()) { + if (range == null || range.length < 2) continue; + if (!isCommentRange(text, range[0])) continue; + + int a = Math.max(range[0], start) - start; + int b = Math.min(range[1], end) - start; + if (b <= 0 || a >= chars.length) continue; + if (a < 0) a = 0; + if (b > chars.length) b = chars.length; + + for (int i = a; i < b; i++) { + char ch = chars[i]; + if (ch != '\n' && ch != '\r') { + chars[i] = ' '; + } + } + } + expr = new String(chars); + } expr = expr.replaceAll("\\s+", " ").trim(); - return expr; } + + private boolean isCommentRange(String text, int start) { + if (start < 0 || start + 1 >= text.length()) return false; + if (text.charAt(start) != '/') return false; + char next = text.charAt(start + 1); + return next == '/' || next == '*'; + } + + /** + * Jump to just before the excluded range that contains position, or return position if unknown. + */ + private int jumpBeforeExcluded(int position) { + if (document == null) return position; + for (int[] range : document.getExcludedRanges()) { + if (position >= range[0] && position < range[1]) { + return range[0] - 1; + } + } + return position; + } + + /** + * Find the matching opening delimiter scanning backward from closePos. + * Skips excluded ranges and ignores delimiters inside string literals. + */ + private int findMatchingBackward(String text, int closePos, char openChar, char closeChar) { + int depth = 0; + boolean inString = false; + char stringChar = 0; + + for (int i = closePos; i >= 0; i--) { + char c = text.charAt(i); + + // Handle strings (backward), respecting escapes + if (!inString && (c == '"' || c == '\'')) { + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) backslashCount++; + if (backslashCount % 2 == 0) { + inString = true; + stringChar = c; + } + } else if (inString && c == stringChar) { + int backslashCount = 0; + for (int j = i - 1; j >= 0 && text.charAt(j) == '\\'; j--) backslashCount++; + if (backslashCount % 2 == 0) { + inString = false; + } + } + + if (inString) continue; + + if (document != null && document.isExcluded(i)) continue; + + if (c == closeChar) depth++; + else if (c == openChar) { + depth--; + if (depth == 0) return i; + } + } + return -1; + } /** * Find dot position before cursor, skipping whitespace. From d63a1b8d4c0ea2aa9d05af429e8b477a735d60af Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 05:50:57 +0200 Subject: [PATCH 318/337] hussar bukkit fixes --- .../java/kamkeel/npcs/util/BukkitUtil.java | 132 ++++++++++-------- .../java/kamkeel/npcs/util/VaultUtil.java | 12 +- .../noppes/npcs/CustomNpcsPermissions.java | 4 +- 3 files changed, 82 insertions(+), 66 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/BukkitUtil.java b/src/main/java/kamkeel/npcs/util/BukkitUtil.java index 14f6b3609..78cd49c82 100644 --- a/src/main/java/kamkeel/npcs/util/BukkitUtil.java +++ b/src/main/java/kamkeel/npcs/util/BukkitUtil.java @@ -2,8 +2,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bukkit.Bukkit; -import org.bukkit.plugin.RegisteredServiceProvider; + +import java.lang.reflect.Method; /** * Utility class for Bukkit integration detection and common operations. @@ -15,6 +15,24 @@ public class BukkitUtil { private static boolean initialized = false; private static boolean bukkitEnabled = false; + // Bukkit classes + private static Class bukkitClass; + private static Class serverClass; + private static Class servicesManagerClass; + private static Class registeredServiceProviderClass; + private static Class offlinePlayerClass; + private static Class playerClass; + + // Bukkit methods + private static Method getServer; + private static Method getServicesManager; + private static Method getRegistration; + private static Method getProvider; + private static Method getOfflinePlayer; + private static Method getPlayer; + private static Method getPluginManager; + private static Method isPluginEnabled; + /** * Initializes Bukkit integration using reflection. * Should be called during FMLServerStartedEvent to ensure Bukkit is fully loaded. @@ -26,24 +44,25 @@ public static void init() { initialized = true; try { - // Make sure all necessary bukkit methods and classes are available. - Class bukkitClass = Class.forName("org.bukkit.Bukkit"); - Class.forName("org.bukkit.Server"); - Class servicesManagerClass =Class.forName("org.bukkit.plugin.ServicesManager"); - Class registeredServiceProviderClass = Class.forName("org.bukkit.plugin.RegisteredServiceProvider"); - Class.forName("org.bukkit.OfflinePlayer"); - Class.forName("org.bukkit.entity.Player"); - - bukkitClass.getMethod("getServer"); - bukkitClass.getMethod("getServicesManager"); - servicesManagerClass.getMethod("getRegistration", Class.class); - registeredServiceProviderClass.getMethod("getProvider"); - bukkitClass.getMethod("getOfflinePlayer", String.class); - bukkitClass.getMethod("getPlayer", String.class); - bukkitClass.getMethod("getPluginManager"); + // Load Bukkit classes + bukkitClass = Class.forName("org.bukkit.Bukkit"); + serverClass = Class.forName("org.bukkit.Server"); + servicesManagerClass = Class.forName("org.bukkit.plugin.ServicesManager"); + registeredServiceProviderClass = Class.forName("org.bukkit.plugin.RegisteredServiceProvider"); + offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); + playerClass = Class.forName("org.bukkit.entity.Player"); + + // Get Bukkit methods + getServer = bukkitClass.getMethod("getServer"); + getServicesManager = bukkitClass.getMethod("getServicesManager"); + getRegistration = servicesManagerClass.getMethod("getRegistration", Class.class); + getProvider = registeredServiceProviderClass.getMethod("getProvider"); + getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class); + getPlayer = bukkitClass.getMethod("getPlayer", String.class); + getPluginManager = bukkitClass.getMethod("getPluginManager"); Class pluginManagerClass = Class.forName("org.bukkit.plugin.PluginManager"); - pluginManagerClass.getMethod("isPluginEnabled", String.class); + isPluginEnabled = pluginManagerClass.getMethod("isPluginEnabled", String.class); bukkitEnabled = true; logger.info("Bukkit integration enabled"); @@ -51,7 +70,7 @@ public static void init() { // Initialize Vault after Bukkit is confirmed VaultUtil.init(); - } catch (ClassNotFoundException | NoClassDefFoundError e) { + } catch (ClassNotFoundException e) { logger.debug("Bukkit not found, Bukkit integration disabled"); } catch (NoSuchMethodException e) { logger.error("Bukkit API method not found", e); @@ -76,88 +95,91 @@ public static boolean isInitialized() { /** * Checks if a specific Bukkit plugin is enabled. - * * @param pluginName the plugin name to check * @return true if the plugin is enabled */ public static boolean isPluginEnabled(String pluginName) { - if (!isEnabled()) return false; + if (!bukkitEnabled) return false; try { - return Bukkit.getPluginManager().isPluginEnabled(pluginName); + Object pluginManager = getPluginManager.invoke(null); + return (Boolean) isPluginEnabled.invoke(pluginManager, pluginName); } catch (Exception e) { - logger.error("Error checking if plugin is enabled: {}", pluginName, e); + logger.error("Error checking if plugin is enabled: " + pluginName, e); return false; } } - /** - * @param pluginName name of the plugin to get. - * @return {@linkplain org.bukkit.plugin.Plugin Plugin} object for the name. Cast to correct class manually. - */ - public static Object getPlugin(String pluginName) { - if (!isEnabled()) return null; - - try { - return Bukkit.getPluginManager().getPlugin(pluginName); - } catch (Exception e) { - logger.error("Error getting plugin object: {}", pluginName, e); - return null; - } - } - /** * Gets a Bukkit OfflinePlayer by name. - * * @param playerName the player name - * @return the {@link org.bukkit.OfflinePlayer} object, or null if not available + * @return the OfflinePlayer object, or null if not available */ - @SuppressWarnings({"deprecation"}) public static Object getOfflinePlayer(String playerName) { - if (!isEnabled()) return null; + if (!bukkitEnabled) return null; try { - return Bukkit.getOfflinePlayer(playerName); + return getOfflinePlayer.invoke(null, playerName); } catch (Exception e) { - logger.error("Error getting OfflinePlayer: {}", playerName, e); + logger.error("Error getting OfflinePlayer: " + playerName, e); return null; } } /** * Gets a Bukkit Player by name (online players only). - * * @param playerName the player name - * @return the {@link org.bukkit.entity.Player} object, or null if not online or not available + * @return the Player object, or null if not online or not available */ public static Object getPlayer(String playerName) { - if (!isEnabled()) return null; + if (!bukkitEnabled) return null; try { - return Bukkit.getPlayer(playerName); + return getPlayer.invoke(null, playerName); } catch (Exception e) { - logger.error("Error getting Player: {}", playerName, e); + logger.error("Error getting Player: " + playerName, e); return null; } } /** * Gets a service provider from Bukkit's ServicesManager. - * * @param serviceClass the service class to get * @return the service provider, or null if not available */ - public static T getServiceProvider(Class serviceClass) { - if (!isEnabled()) return null; + public static Object getServiceProvider(Class serviceClass) { + if (!bukkitEnabled) return null; try { - RegisteredServiceProvider registration = Bukkit.getServicesManager().getRegistration(serviceClass); + Object servicesManager = getServicesManager.invoke(null); + Object registration = getRegistration.invoke(servicesManager, serviceClass); if (registration != null) { - return registration.getProvider(); + return getProvider.invoke(registration); } } catch (Exception e) { - logger.error("Error getting service provider: {}", serviceClass.getName(), e); + logger.error("Error getting service provider: " + serviceClass.getName(), e); } return null; } + + // Getters for classes (for use by other utils like VaultUtil) + public static Class getBukkitClass() { + return bukkitClass; + } + + public static Class getServicesManagerClass() { + return servicesManagerClass; + } + + public static Class getRegisteredServiceProviderClass() { + return registeredServiceProviderClass; + } + + public static Class getOfflinePlayerClass() { + return offlinePlayerClass; + } + + public static Class getPlayerClass() { + return playerClass; + } } diff --git a/src/main/java/kamkeel/npcs/util/VaultUtil.java b/src/main/java/kamkeel/npcs/util/VaultUtil.java index ab485e186..c7ef92e97 100644 --- a/src/main/java/kamkeel/npcs/util/VaultUtil.java +++ b/src/main/java/kamkeel/npcs/util/VaultUtil.java @@ -3,8 +3,6 @@ import net.minecraft.entity.player.EntityPlayer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bukkit.Bukkit; -import org.bukkit.plugin.Plugin; import java.lang.reflect.Method; @@ -70,19 +68,15 @@ public static void init() { } try { - Class offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer"); + Class offlinePlayerClass = BukkitUtil.getOfflinePlayerClass(); if (offlinePlayerClass == null) { logger.error("Could not get OfflinePlayer class from BukkitUtil"); return; } // Load Vault classes - Plugin vault = Bukkit.getPluginManager().getPlugin("Vault"); - if (vault == null) - return; - ClassLoader vaultLoader = vault.getClass().getClassLoader(); - economyClass = vaultLoader.loadClass("net.milkbowl.vault.economy.Economy"); - economyResponseClass = vaultLoader.loadClass("net.milkbowl.vault.economy.EconomyResponse"); + economyClass = Class.forName("net.milkbowl.vault.economy.Economy"); + economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse"); // Get Economy methods (using deprecated String methods for broader compatibility) isEnabled = economyClass.getMethod("isEnabled"); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 6f99dd8fc..a8ee213ae 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -7,7 +7,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import org.apache.logging.log4j.LogManager; -import org.bukkit.entity.Player; +//import org.bukkit.entity.Player; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -180,7 +180,7 @@ private boolean bukkitPermission(String username, String permission) { try { Object player = BukkitUtil.getPlayer(username); if (player == null) return false; - return ((Player) player).hasPermission(permission); + return false;//((Player) player).hasPermission(permission); } catch (Exception e) { e.printStackTrace(); } From 4e78ffb024082dfe5b40ad688a2d033dfcd306cf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 06:54:09 +0200 Subject: [PATCH 319/337] Fix autocomplete trigger after dot --- .../autocomplete/AutocompleteManager.java | 52 ++++++++++++++----- .../autocomplete/JSAutocompleteProvider.java | 2 +- .../JavaAutocompleteProvider.java | 51 ++++++++++++++---- 3 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index e5d13f286..d6196edc4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -179,18 +179,27 @@ public void onCharTyped(char c, String text, int cursorPosition) { // Check if we're typing an identifier if (Character.isJavaIdentifierPart(c)) { + // GuiScriptTextArea passes a cursor position captured BEFORE insertion while `text` + // is already the post-insert text. When this happens, cursorPosition points at the + // newly inserted character, not the caret-after-insert. Normalize to a caret position + // for prefix extraction/update. + int effectiveCursor = cursorPosition; + if (effectiveCursor >= 0 && effectiveCursor < text.length() && text.charAt(effectiveCursor) == c) { + effectiveCursor = effectiveCursor + 1; + } + if (active) { // Update existing autocomplete - updatePrefix(text, cursorPosition); + updatePrefix(text, effectiveCursor); } else if (Character.isJavaIdentifierStart(c)) { // Check if we're after a dot with whitespace (e.g., "obj. |" where | is cursor) - int dotPos = findDotBeforeWhitespace(text, cursorPosition - 1); + int dotPos = findDotBeforeWhitespace(text, effectiveCursor - 1); if (dotPos >= 0) { // We're typing after a dot (with possible whitespace), trigger member access - triggerAfterDot(text, cursorPosition); + triggerAfterDot(text, effectiveCursor); } else { // Potentially start new autocomplete - maybeStartAutocomplete(text, cursorPosition, false); + maybeStartAutocomplete(text, effectiveCursor, false); } } return; @@ -283,29 +292,39 @@ public void triggerExplicit() { * Trigger autocomplete after a dot is typed. */ private void triggerAfterDot(String text, int cursorPosition) { - // Find the receiver expression before the dot - // First check if immediately before cursor - int dotPos = cursorPosition - 1; - if (dotPos < 0 || text.charAt(dotPos) != '.') { - // Look backwards skipping whitespace to find dot - dotPos = findDotBeforeWhitespace(text, cursorPosition - 1); + // Normalize cursor semantics. + // Some callers pass a pre-insert cursor index (points at '.') even though the caret + // is visually after the inserted '.' in the current text. + int caretPos = Math.max(0, Math.min(cursorPosition, text.length())); + + int dotPos = -1; + if (caretPos > 0 && caretPos <= text.length() && text.charAt(caretPos - 1) == '.') { + // Caret-after-dot case + dotPos = caretPos - 1; + } else if (caretPos >= 0 && caretPos < text.length() && text.charAt(caretPos) == '.') { + // Pre-insert cursor case: cursorPosition points at the dot itself + dotPos = caretPos; + caretPos = Math.min(caretPos + 1, text.length()); + } else { + // Look backwards skipping whitespace/identifier to find dot + dotPos = findDotBeforeWhitespace(text, caretPos - 1); } if (dotPos < 0) return; String receiverExpr = findReceiverExpression(text, dotPos); - String prefix = findCurrentWord(text, cursorPosition); + String prefix = findCurrentWord(text, caretPos); // Find where the prefix actually starts (after dot + any whitespace) int prefixStart = dotPos + 1; - while (prefixStart < cursorPosition && Character.isWhitespace(text.charAt(prefixStart))) { + while (prefixStart < caretPos && Character.isWhitespace(text.charAt(prefixStart))) { prefixStart++; } prefixStartPosition = prefixStart; currentPrefix = prefix; - showSuggestions(text, cursorPosition, prefix, prefixStartPosition, true, receiverExpr); + showSuggestions(text, caretPos, prefix, prefixStartPosition, true, receiverExpr); } /** @@ -379,7 +398,12 @@ private void showSuggestions(String text, int cursorPosition, String prefix, // Resolve receiver type if member access if (isMemberAccess && receiverExpr != null) { - TypeInfo receiverType = document.resolveExpressionType(receiverExpr, prefixStart); + // Use a position inside the receiver/scope for resolution. + // `prefixStart` can land on end-exclusive boundaries (e.g., right at bodyEnd), + // causing findInnermostScopeAt(prefixStart) to return null. + int dotPos = findDotBeforeWhitespace(text, prefixStart - 1); + int resolvePos = dotPos >= 0 ? dotPos : prefixStart; + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, resolvePos); if (receiverType != null && receiverType.isResolved()) { currentReceiverFullName = receiverType.getFullName(); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index 2fd28c4b6..1ba3d9769 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -40,7 +40,7 @@ protected void addMemberSuggestions(Context context, List item } // Use ScriptDocument's resolveExpressionType - handles both Java and JS - TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart); + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context)); if (receiverType == null || !receiverType.isResolved()) { return; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java index 81e105b4b..7756376aa 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JavaAutocompleteProvider.java @@ -43,13 +43,14 @@ public List getSuggestions(Context context) { String ownerFullName = null; boolean isStaticContext = false; if (context.isMemberAccess && context.receiverExpression != null) { + int resolvePos = getMemberAccessResolvePosition(context); TypeInfo receiverType = document.resolveExpressionType( - context.receiverExpression, context.prefixStart); + context.receiverExpression, resolvePos); if (receiverType != null && receiverType.isResolved()) { ownerFullName = receiverType.getFullName(); } // Check if accessing through a class (static context) vs instance - isStaticContext = isStaticAccess(context.receiverExpression, context.prefixStart); + isStaticContext = isStaticAccess(context.receiverExpression, resolvePos); } if (context.isMemberAccess) { @@ -79,14 +80,14 @@ protected void addMemberSuggestions(Context context, List item } // Resolve the type of the receiver expression - TypeInfo receiverType = document.resolveExpressionType(receiverExpr, context.prefixStart); + TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context)); if (receiverType == null || !receiverType.isResolved()) { return; } // Determine if this is a static context (accessing a class type) - boolean isStaticContext = isStaticAccess(receiverExpr, context.prefixStart); + boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context)); Class clazz = receiverType.getJavaClass(); if (clazz == null) { @@ -129,6 +130,36 @@ protected void addMemberSuggestions(Context context, List item } } + + /** + * Pick a stable position for resolving member-access receiver types. + * + * Using `prefixStart` can land on end-exclusive scope boundaries (position == bodyEnd) + * depending on caller cursor semantics and surrounding syntax, causing scope lookups + * to fail. For member access we prefer the dot position (or nearest non-whitespace) + * immediately before prefixStart. + */ + protected int getMemberAccessResolvePosition(Context context) { + if (context == null || context.text == null || context.text.isEmpty()) { + return 0; + } + + int pos = Math.max(0, Math.min(context.prefixStart, context.text.length())); + int i = Math.min(pos - 1, context.text.length() - 1); + + // Skip whitespace backwards + while (i >= 0 && Character.isWhitespace(context.text.charAt(i))) { + i--; + } + + // Prefer the dot index if present + if (i >= 0 && context.text.charAt(i) == '.') { + return i; + } + + // Fallback: use the provided prefixStart (caret context) + return Math.max(0, Math.min(context.prefixStart, context.text.length())); + } /** * Add members from a script-defined type. @@ -184,10 +215,10 @@ protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List items) { int pos = context.cursorPosition; - + // Find containing method MethodInfo containingMethod = document.findContainingMethod(pos); - + // Add local variables if (containingMethod != null) { Map locals = document.getLocalsForMethod(containingMethod); @@ -198,20 +229,20 @@ protected void addScopeSuggestions(Context context, List items } } } - + // Add method parameters for (FieldInfo param : containingMethod.getParameters()) { items.add(AutocompleteItem.fromField(param)); } } - + // Add global fields for (FieldInfo globalField : document.getGlobalFields().values()) { if (globalField.isVisibleAt(pos)) { items.add(AutocompleteItem.fromField(globalField)); } } - + // Add enclosing type fields (find via script types map) ScriptTypeInfo enclosingType = findEnclosingType(pos); if (enclosingType != null) { @@ -220,7 +251,7 @@ protected void addScopeSuggestions(Context context, List items items.add(AutocompleteItem.fromField(field)); } } - + // Add methods from enclosing type for (List overloads : enclosingType.getMethods().values()) { for (MethodInfo method : overloads) { From 074c780d50e92b38e68dffe2ca826399d5e12d5c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 07:03:33 +0200 Subject: [PATCH 320/337] Add inner callable scope locals and expression type resolution ScriptDocument now tracks locals inside lambdas/function expressions and resolves identifiers by consulting the innermost callable scope before falling back to method/global scope. Adds expression resolver nodes/type resolver and wires position-aware resolution for lambda bodies, plus basic lambda operator marking and return type validation against expected functional interface SAM. --- .../script/interpreter/ScriptDocument.java | 566 +++++++++++++++++- .../expression/ExpressionNode.java | 127 ++++ .../expression/ExpressionParser.java | 231 ++++++- .../expression/ExpressionToken.java | 7 +- .../expression/ExpressionTokenizer.java | 17 + .../expression/ExpressionTypeResolver.java | 281 +++++++++ .../script/interpreter/field/FieldInfo.java | 6 +- 7 files changed, 1208 insertions(+), 27 deletions(-) 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 a12bfff5a..e7dd78a6f 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 @@ -467,6 +467,15 @@ public boolean isExcludedInclusive(int position) { } return false; } + + /** + * Get the list of excluded ranges (comments, strings, etc.). + * Used by AutocompleteManager to skip over excluded regions. + * @return List of [start, end) ranges to exclude + */ + public List getExcludedRanges() { + return excludedRanges; + } /** * Check if a position is within a comment range (not string). @@ -1330,6 +1339,144 @@ private void parseLocalVariables() { } } } + + // Also parse locals inside inner callable scopes + for (InnerCallableScope scope : innerScopes) { + parseLocalVariablesInScope(scope); + } + } + + /** + * Parse local variable declarations inside an inner callable scope (lambda or JS function expression). + */ + private void parseLocalVariablesInScope(InnerCallableScope scope) { + int start = scope.getBodyStart(); + int end = scope.getBodyEnd(); + + if (start < 0 || end <= start) return; + + if (isJavaScript()) { + parseJSLocalsInRange(start, end, scope); + } else { + parseJavaLocalsInRange(start, end, scope); + } + } + + /** + * Parse Java local variable declarations in the range [start, end). + * Pattern: Type varName = ... or var varName = ... + */ + private void parseJavaLocalsInRange(int start, int end, InnerCallableScope scope) { + String rangeText = text.substring(start, Math.min(end, text.length())); + + Matcher m = FIELD_DECL_PATTERN.matcher(rangeText); + while (m.find()) { + int declPos = start + m.start(); + if (declPos < start || declPos >= end) continue; + if (isExcluded(declPos)) continue; + + String typeNameRaw = m.group(1); + String varName = m.group(2); + String delimiter = m.group(3); + + // Skip control flow keywords + if (typeNameRaw.equals("return") || typeNameRaw.equals("if") || typeNameRaw.equals("while") || + typeNameRaw.equals("for") || typeNameRaw.equals("switch") || typeNameRaw.equals("catch") || + typeNameRaw.equals("new") || typeNameRaw.equals("throw")) { + continue; + } + + String typeName = stripModifiers(typeNameRaw); + int modifiers = parseModifiers(typeNameRaw); + + TypeInfo varType = null; + if (!typeName.equals("var") && !typeName.equals("let") && !typeName.equals("const")) { + varType = resolveType(typeName); + } else if (delimiter.equals("=")) { + // Infer type from initializer + int rhsStart = start + m.end(); + varType = inferTypeFromExpression(rhsStart); + } + + int initStart = -1, initEnd = -1; + if ("=".equals(delimiter)) { + initStart = start + m.start(3); + int searchPos = start + m.end(3); + int depth = 0; + int angleDepth = 0; + while (searchPos < end) { + char c = text.charAt(searchPos); + if (c == '(' || c == '[' || c == '{') depth++; + else if (c == ')' || c == ']' || c == '}') depth--; + else if (c == '<') angleDepth++; + else if (c == '>') angleDepth--; + else if ((c == ';' || c == ',') && depth == 0 && angleDepth == 0) { + initEnd = searchPos; + break; + } + searchPos++; + } + } + + int absPos = start + m.start(2); + FieldInfo localVar = FieldInfo.localField(varName, varType, absPos, null, initStart, initEnd, modifiers); + scope.addLocal(varName, localVar); + } + } + + /** + * Parse JS local variable declarations in the range [start, end). + * Pattern: var/let/const varName = ... + */ + private void parseJSLocalsInRange(int start, int end, InnerCallableScope scope) { + String rangeText = text.substring(start, Math.min(end, text.length())); + + Pattern varPattern = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*(=)\\s*([^;\\n]+))?"); + Matcher m = varPattern.matcher(rangeText); + + while (m.find()) { + int declPos = start + m.start(); + if (declPos < start || declPos >= end) continue; + if (isExcluded(declPos)) continue; + + String varName = m.group(1); + String initializer = m.group(3); + + // Check for JSDoc type annotation + JSDocInfo jsDoc = jsDocParser.extractJSDocBefore(text, declPos); + + TypeInfo varType = null; + + // Priority 1: JSDoc @type + if (jsDoc != null && jsDoc.hasTypeTag()) { + varType = jsDoc.getDeclaredType(); + } + + // Priority 2: Infer from initializer + if (varType == null && initializer != null && !initializer.trim().isEmpty()) { + varType = resolveExpressionType(initializer.trim(), start + m.start(3)); + } + + // Priority 3: Use "any" type + if (varType == null) { + varType = TypeInfo.ANY; + } + + int initStart = -1, initEnd = -1; + if (m.group(2) != null) { + initStart = start + m.start(2); + initEnd = start + m.end(3); + } + + int absPos = start + m.start(1); + FieldInfo localVar = FieldInfo.localField(varName, varType, absPos, null, initStart, initEnd, 0); + + if (jsDoc != null) { + localVar.setJSDocInfo(jsDoc); + } + + scope.addLocal(varName, localVar); + } } /** @@ -2429,12 +2576,226 @@ private List buildMarks() { markUnusedImports(marks); } + // Mark lambda and method reference operators (-> and ::) + markLambdaOperators(marks); + + // Mark lambda/function parameters with type info + markInnerScopeParameters(marks); + + // Validate lambda return types + validateLambdaReturnTypes(marks); + // Final pass: Mark any remaining unmarked identifiers as undefined markUndefinedIdentifiers(marks); return marks; } + /** + * Mark lambda arrow (->) and method reference (::) operators. + */ + private void markLambdaOperators(List marks) { + // Mark -> operators (lambda arrows) + for (int i = 0; i < text.length() - 1; i++) { + if (text.charAt(i) == '-' && text.charAt(i + 1) == '>') { + if (!isExcluded(i)) { + marks.add(new ScriptLine.Mark(i, i + 2, TokenType.KEYWORD)); + } + } + } + + // Mark :: operators (method references) + for (int i = 0; i < text.length() - 1; i++) { + if (text.charAt(i) == ':' && text.charAt(i + 1) == ':') { + if (!isExcluded(i)) { + marks.add(new ScriptLine.Mark(i, i + 2, TokenType.KEYWORD)); + } + } + } + } + + /** + * Mark parameters in all inner callable scopes (lambdas and JS function expressions). + */ + private void markInnerScopeParameters(List marks) { + for (InnerCallableScope scope : innerScopes) { + for (FieldInfo param : scope.getParameters()) { + int start = param.getDeclarationOffset(); + int end = start + param.getName().length(); + + // Use FieldInfo as metadata - hover system will extract tooltip + marks.add(new ScriptLine.Mark(start, end, TokenType.PARAMETER, param)); + } + } + } + + /** + * Validate lambda return types against expected SAM return type. + */ + private void validateLambdaReturnTypes(List marks) { + for (InnerCallableScope scope : innerScopes) { + if (scope.getKind() == InnerCallableScope.Kind.JAVA_LAMBDA || + scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR) { + validateLambdaReturnType(scope, marks); + } + } + } + + /** + * Validate that a lambda's body return type matches the expected SAM return type. + */ + private void validateLambdaReturnType(InnerCallableScope lambda, List marks) { + TypeInfo expectedType = lambda.getExpectedType(); + if (expectedType == null || !expectedType.isFunctionalInterface()) { + return; // No expected type to validate against + } + + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam == null) { + return; + } + + TypeInfo expectedReturnType = sam.getReturnType(); + + int bodyStart = lambda.getBodyStart(); + int bodyEnd = lambda.getBodyEnd(); + if (bodyStart < 0 || bodyEnd <= bodyStart || bodyEnd > text.length()) { + return; + } + + String bodyText = text.substring(bodyStart, bodyEnd).trim(); + + if (bodyText.startsWith("{")) { + // Block lambda - validate return statements + validateBlockLambdaReturns(lambda, bodyStart, bodyEnd, expectedReturnType, marks); + } else { + // Expression lambda - validate expression type + try { + TypeInfo bodyType = resolveExpressionType(bodyText, bodyStart); + + if (bodyType != null && expectedReturnType != null) { + // Check compatibility + if (!isCompatibleType(bodyType, expectedReturnType)) { + // Mark error at body start + String error = "Incompatible return type: expected " + + expectedReturnType.getSimpleName() + + " but was " + bodyType.getSimpleName(); + marks.add(new ScriptLine.Mark(bodyStart, bodyEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } + } + } catch (Exception e) { + // Fail soft - don't crash on malformed expressions + } + } + } + + /** + * Validate return statements in a block lambda (one with { }). + * Checks that all return statements return compatible types with the expected SAM return type. + */ + private void validateBlockLambdaReturns(InnerCallableScope lambda, int blockStart, int blockEnd, + TypeInfo expectedReturnType, List marks) { + // Find all return statements in the block + String blockText = text.substring(blockStart, blockEnd); + Pattern returnPattern = Pattern.compile("\\breturn\\b(\\s*;|\\s+[^;]+;)"); + Matcher m = returnPattern.matcher(blockText); + + while (m.find()) { + int returnStart = blockStart + m.start(); + int returnEnd = blockStart + m.end(); + + if (isExcluded(returnStart)) { + continue; + } + + // Get the return expression (if any) + String returnStmt = m.group(1).trim(); + + if (returnStmt.equals(";")) { + // return; - void return + if (expectedReturnType != null && !expectedReturnType.getSimpleName().equals("void")) { + String error = "Cannot return void from lambda expecting " + expectedReturnType.getSimpleName(); + marks.add(new ScriptLine.Mark(returnStart, returnEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } + } else { + // return ; - typed return + String expr = returnStmt.substring(0, returnStmt.length() - 1).trim(); + + if (expectedReturnType != null && expectedReturnType.getSimpleName().equals("void")) { + String error = "Cannot return a value from void lambda"; + marks.add(new ScriptLine.Mark(returnStart, returnEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } else { + try { + // Validate return expression type + int exprStart = returnStart + m.group(0).indexOf(expr); + TypeInfo returnType = resolveExpressionType(expr, exprStart); + + if (returnType != null && expectedReturnType != null) { + if (!isCompatibleType(returnType, expectedReturnType)) { + String error = "Incompatible return type: expected " + + expectedReturnType.getSimpleName() + + " but returned " + returnType.getSimpleName(); + int exprEnd = exprStart + expr.length(); + marks.add(new ScriptLine.Mark(exprStart, exprEnd, TokenType.UNDEFINED_VAR, TokenErrorMessage.from(error))); + } + } + } catch (Exception e) { + // Fail soft on malformed expressions + } + } + } + } + + // Check for missing returns in non-void lambdas + // This is a warning rather than an error - Java allows it if the lambda throws or has infinite loop + // We skip this validation to avoid false positives + } + + /** + * Check if an actual type is compatible with an expected type. + * Includes boxing, subtype checking, and special cases like void. + */ + private boolean isCompatibleType(TypeInfo actual, TypeInfo expected) { + if (actual == null || expected == null) return false; + if (actual.equals(expected)) return true; + if (actual.getSimpleName().equals(expected.getSimpleName())) return true; + + // void is compatible with anything (statement lambda) + if (expected.getSimpleName().equals("void")) return true; + + // Object is compatible with anything + if (expected.getSimpleName().equals("Object") || expected.getFullName().equals("java.lang.Object")) return true; + + // Check primitive boxing + if (isBoxingCompatible(actual.getSimpleName(), expected.getSimpleName())) return true; + + // Use TypeChecker for more complex compatibility + return TypeChecker.isTypeCompatible(expected, actual); + } + + /** + * Check if types are compatible via boxing/unboxing. + */ + private boolean isBoxingCompatible(String actualName, String expectedName) { + if (actualName.equals("int") && expectedName.equals("Integer")) return true; + if (actualName.equals("Integer") && expectedName.equals("int")) return true; + if (actualName.equals("boolean") && expectedName.equals("Boolean")) return true; + if (actualName.equals("Boolean") && expectedName.equals("boolean")) return true; + if (actualName.equals("long") && expectedName.equals("Long")) return true; + if (actualName.equals("Long") && expectedName.equals("long")) return true; + if (actualName.equals("double") && expectedName.equals("Double")) return true; + if (actualName.equals("Double") && expectedName.equals("double")) return true; + if (actualName.equals("float") && expectedName.equals("Float")) return true; + if (actualName.equals("Float") && expectedName.equals("float")) return true; + if (actualName.equals("byte") && expectedName.equals("Byte")) return true; + if (actualName.equals("Byte") && expectedName.equals("byte")) return true; + if (actualName.equals("short") && expectedName.equals("Short")) return true; + if (actualName.equals("Short") && expectedName.equals("short")) return true; + if (actualName.equals("char") && expectedName.equals("Character")) return true; + if (actualName.equals("Character") && expectedName.equals("char")) return true; + return false; + } + /** * Find and mark unused imports as UNUSED_IMPORT type. * This must be called after all other mark building is complete. @@ -4403,7 +4764,8 @@ private TypeInfo resolveExpressionWithParserAPI(String expr, int position) { ExpressionNode.TypeResolverContext context = createExpressionResolverContext(position); // Use the expression resolver to parse and resolve the type - ExpressionTypeResolver resolver = new ExpressionTypeResolver(context); + // Pass the position as basePosition so lambda position calculations work correctly + ExpressionTypeResolver resolver = new ExpressionTypeResolver(context, this, position); TypeInfo result = resolver.resolve(expr); // Special case: null literal type is "unresolved" but valid @@ -4744,10 +5106,17 @@ int[] findReceiverBoundsBefore(int dotIndex) { if (pos < 0) return null; while (pos >= 0) { - // Check if we're in a comment range (but not string range) - // Skip comments to avoid picking up types from comment text - if (isInCommentRange(pos)) { - return null; + // Check if we're in an excluded range (comment, string, etc.) + // Skip excluded regions to avoid picking up types from comment text + if (isExcluded(pos)) { + // Find the excluded range and skip to before it + for (int[] range : excludedRanges) { + if (pos >= range[0] && pos < range[1]) { + pos = range[0] - 1; // Jump to before the excluded range + break; + } + } + continue; // Continue scanning from before the excluded range } char c = text.charAt(pos); @@ -4769,8 +5138,11 @@ int[] findReceiverBoundsBefore(int dotIndex) { if (Character.isJavaIdentifierPart(c)) { while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) pos--; + // Skip whitespace to check for chained identifier (preceded by a dot) + int checkPos = pos; + while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) checkPos--; // If it's part of a chained identifier (preceded by a dot), continue - if (pos >= 0 && text.charAt(pos) == '.') { pos--; continue; } + if (checkPos >= 0 && text.charAt(checkPos) == '.') { pos = checkPos - 1; continue; } break; } @@ -5045,13 +5417,32 @@ private void markVariables(List marks) { boolean isUppercase = Character.isUpperCase(name.charAt(0)); // Scope resolution order: - // 1. Method parameters (if inside method) - // 2. Method local variables (if inside method) - // 3. Enclosing type fields (if inside method) - // 4. Global fields - // 5. Script type fields + // 1. Inner callable scope parameters (lambda/function expressions) + // 2. Inner callable scope locals + // 3. Method parameters (if inside method) + // 4. Method local variables (if inside method) + // 5. Enclosing type fields (if inside method) + // 6. Global fields + // 7. Script type fields + + // Check inner callable scope parameters first (lambda/function expressions) + Object innermostScope = findInnermostScopeAt(m.start(1)); + if (innermostScope instanceof InnerCallableScope) { + InnerCallableScope innerScope = (InnerCallableScope) innermostScope; + FieldInfo innerParam = innerScope.getParameter(name); + if (innerParam != null) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER, innerParam)); + continue; + } + // Also check locals + FieldInfo innerLocal = innerScope.getLocals().get(name); + if (innerLocal != null) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD, innerLocal)); + continue; + } + } - // Check parameters first (method scope) + // Check parameters (method scope) - fallback if not in inner scope if (containingMethod != null && containingMethod.hasParameter(name)) { FieldInfo paramInfo = containingMethod.getParameter(name); marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.PARAMETER, paramInfo)); @@ -6065,19 +6456,49 @@ FieldAccessInfo createFieldAccessInfo(String name, int start, int end, /** * Resolve a variable by name at a given position. * Works for BOTH Java and JavaScript using unified data structures. + * Checks innermost scope first (lambda or method), then walks up parent chain. */ public FieldInfo resolveVariable(String name, int position) { - // Find containing method/function - MethodInfo containingMethod = findMethodAtPosition(position); + // 1. Check innermost scope (lambda or method) + Object innermostScope = findInnermostScopeAt(position); + + if (innermostScope instanceof InnerCallableScope) { + InnerCallableScope scope = (InnerCallableScope) innermostScope; + + // Check lambda parameters and locals, walking up parent chain + InnerCallableScope currentScope = scope; + while (currentScope != null) { + // Check parameters + FieldInfo param = currentScope.getParameter(name); + if (param != null) { + return param; + } + + // Check locals + FieldInfo local = currentScope.getLocals().get(name); + if (local != null) { + return local; + } + + currentScope = currentScope.getParentScope(); + } + + // Not found in lambda scopes - fall back to enclosing method + // Find which method this lambda is inside + innermostScope = findMethodAtPosition(position); + } - if (containingMethod != null) { - // Check parameters - if (containingMethod.hasParameter(name)) { - return containingMethod.getParameter(name); + // 2. Check method scope (params + locals) + if (innermostScope instanceof MethodInfo) { + MethodInfo method = (MethodInfo) innermostScope; + + // Check method parameters + if (method.hasParameter(name)) { + return method.getParameter(name); } - // Check local variables (methodLocals stores both Java locals and JS var/let/const inside functions) - Map locals = methodLocals.get(containingMethod.getDeclarationOffset()); + // Check method locals + Map locals = methodLocals.get(method.getDeclarationOffset()); if (locals != null && locals.containsKey(name)) { FieldInfo localInfo = locals.get(name); if (localInfo.isVisibleAt(position)) { @@ -6086,7 +6507,7 @@ public FieldInfo resolveVariable(String name, int position) { } } - // For Java only: Check if we're inside a script type and look for fields there + // 3. For Java only: Check if we're inside a script type and look for fields there if (!isJavaScript()) { ScriptTypeInfo enclosingType = findEnclosingScriptType(position); if (enclosingType != null && enclosingType.hasField(name)) { @@ -6094,12 +6515,12 @@ public FieldInfo resolveVariable(String name, int position) { } } - // Check global fields (stores both Java global fields and JS global var/let/const) + // 4. Check global fields (stores both Java global fields and JS global var/let/const) if (globalFields.containsKey(name)) { return globalFields.get(name); } - // Check JS global objects from JSTypeRegistry (like API, DBCAPI) + // 5. Check JS global objects from JSTypeRegistry (like API, DBCAPI) if (isJavaScript()) { JSTypeRegistry registry = JSTypeRegistry.getInstance(); String globalObjectType = registry.getGlobalObjectType(name); @@ -6348,6 +6769,105 @@ public Map getGlobalFields() { return Collections.unmodifiableMap(globalFields); } + /** + * Get all variables available at a specific position. + * Used by autocomplete to show scope-aware suggestions. + * Returns variables from innermost scope first (parameters, then locals), + * walking up through parent scopes, then method scope, then globals. + * + * @param position The cursor position + * @return List of FieldInfo for all available variables, ordered by priority + */ + public List getAvailableVariablesAt(int position) { + List variables = new ArrayList<>(); + + // Check innermost scope (lambda or method) + Object innermostScope = findInnermostScopeAt(position); + + if (innermostScope instanceof InnerCallableScope) { + InnerCallableScope scope = (InnerCallableScope) innermostScope; + + // Add lambda parameters and locals, walking up parent chain + InnerCallableScope currentScope = scope; + while (currentScope != null) { + // Add parameters from this scope + variables.addAll(currentScope.getParameters()); + + // Add locals from this scope (only those visible at position) + for (FieldInfo local : currentScope.getLocals().values()) { + if (local.isVisibleAt(position)) { + variables.add(local); + } + } + + currentScope = currentScope.getParentScope(); + } + + // Fall back to enclosing method + innermostScope = findMethodAtPosition(position); + } + + // Add method scope variables + if (innermostScope instanceof MethodInfo) { + MethodInfo method = (MethodInfo) innermostScope; + + // Add method parameters + variables.addAll(method.getParameters()); + + // Add method locals (only those visible at position) + Map locals = methodLocals.get(method.getDeclarationOffset()); + if (locals != null) { + for (FieldInfo local : locals.values()) { + if (local.isVisibleAt(position)) { + variables.add(local); + } + } + } + } + + // Add global fields (only those visible at position) + for (FieldInfo globalField : globalFields.values()) { + if (globalField.isVisibleAt(position)) { + variables.add(globalField); + } + } + + // For Java: add fields from enclosing type + if (!isJavaScript()) { + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null) { + for (FieldInfo field : enclosingType.getFields().values()) { + if (field.isVisibleAt(position)) { + variables.add(field); + } + } + } + } + + // For JS: add global engine objects and editor globals + if (isJavaScript()) { + JSTypeRegistry registry = JSTypeRegistry.getInstance(); + + // Add global engine objects (like API, DBCAPI) + for (String globalName : registry.getGlobalEngineObjects().keySet()) { + FieldInfo field = resolveVariable(globalName, position); + if (field != null && field.isResolved()) { + variables.add(field); + } + } + + // Add editor/DataScript global variables + for (String globalName : editorGlobals.keySet()) { + FieldInfo field = resolveVariable(globalName, position); + if (field != null && field.isResolved()) { + variables.add(field); + } + } + } + + return variables; + } + /** * Get all errored assignments across all fields (global, local, and external). * Used by ScriptLine to draw error underlines. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java index 85bcf1f30..f3d588f4d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionNode.java @@ -224,6 +224,133 @@ public static class ParenthesizedNode extends ExpressionNode { public TypeInfo resolveType(TypeResolverContext resolver) { return inner.resolveType(resolver); } } + public static class LambdaNode extends ExpressionNode { + private final List parameterNames; + private final ExpressionNode body; + private final boolean isBlock; + private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef; + + public LambdaNode(List parameterNames, ExpressionNode body, boolean isBlock, int start, int end) { + super(start, end); + this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>(); + this.body = body; + this.isBlock = isBlock; + this.scopeRef = null; + } + + public List getParameterNames() { return Collections.unmodifiableList(parameterNames); } + public ExpressionNode getBody() { return body; } + public boolean isBlock() { return isBlock; } + public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; } + public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) { + this.scopeRef = scope; + } + + @Override + public TypeInfo resolveType(TypeResolverContext resolver) { + // Lambda type is determined by the expected functional interface type + // See ExpressionTypeResolver.resolveLambdaType() + return TypeInfo.fromClass(Object.class); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (parameterNames.size() == 1 && !isBlock) { + sb.append(parameterNames.get(0)); + } else { + sb.append("("); + sb.append(String.join(", ", parameterNames)); + sb.append(")"); + } + sb.append(" -> "); + if (isBlock) { + sb.append("{ ... }"); + } else { + sb.append(body); + } + return sb.toString(); + } + } + + public static class JSFunctionNode extends ExpressionNode { + private final String name; // Optional name for named function expressions + private final List parameterNames; + private final String bodyText; // Raw body text (for block functions) + private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef; + + public JSFunctionNode(String name, List parameterNames, String bodyText, int start, int end) { + super(start, end); + this.name = name; + this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>(); + this.bodyText = bodyText; + this.scopeRef = null; + } + + public String getName() { return name; } + public List getParameterNames() { return Collections.unmodifiableList(parameterNames); } + public String getBodyText() { return bodyText; } + public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; } + public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) { + this.scopeRef = scope; + } + + @Override + public TypeInfo resolveType(TypeResolverContext resolver) { + // JS function type is determined by the expected functional interface type + // See ExpressionTypeResolver.resolveJSFunctionType() + return TypeInfo.fromClass(Object.class); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("function"); + if (name != null && !name.isEmpty()) { + sb.append(" ").append(name); + } + sb.append("("); + sb.append(String.join(", ", parameterNames)); + sb.append(") { "); + if (bodyText.length() > 30) { + sb.append(bodyText.substring(0, 30)).append("..."); + } else { + sb.append(bodyText); + } + sb.append(" }"); + return sb.toString(); + } + } + + public static class MethodReferenceNode extends ExpressionNode { + private final ExpressionNode target; // Object or class name + private final String methodName; + private final boolean isStatic; // Whether it's Class::method vs obj::method + + public MethodReferenceNode(ExpressionNode target, String methodName, boolean isStatic, int start, int end) { + super(start, end); + this.target = target; + this.methodName = methodName; + this.isStatic = isStatic; + } + + public ExpressionNode getTarget() { return target; } + public String getMethodName() { return methodName; } + public boolean isStatic() { return isStatic; } + + @Override + public TypeInfo resolveType(TypeResolverContext resolver) { + // Method reference type is determined by the expected functional interface type + // See ExpressionTypeResolver.resolveMethodReferenceType() + return TypeInfo.fromClass(Object.class); + } + + @Override + public String toString() { + return target + "::" + methodName; + } + } + public interface TypeResolverContext { TypeInfo resolveIdentifier(String name); TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java index 5bb2f9fd7..dacd60618 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java @@ -14,7 +14,119 @@ public ExpressionParser(List tokens) { public ExpressionNode parse() { if (tokens.isEmpty()) return null; - return parseExpression(0); + return parseLambdaOrExpression(); + } + + private ExpressionNode parseLambdaOrExpression() { + // Check if this is a lambda: (params) -> or param -> + // Look ahead to see if there's a -> after the next identifier/parens + + int checkpoint = pos; + List paramNames = null; + int lambdaStart = current().getStart(); + + // Try to parse parameter list + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + // Single param without parens + String paramName = current().getText(); + advance(); + if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) { + paramNames = new ArrayList<>(); + paramNames.add(paramName); + } else { + // Not a lambda, rewind + pos = checkpoint; + } + } else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { + // Parenthesized params: (a, b) or () + advance(); // consume '(' + paramNames = new ArrayList<>(); + + if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + // Parse param list + while (true) { + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) { + // Not a valid lambda param list, rewind + pos = checkpoint; + paramNames = null; + break; + } + paramNames.add(current().getText()); + advance(); + + if (check(ExpressionToken.TokenKind.COMMA)) { + advance(); + } else if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + break; + } else { + // Invalid syntax, rewind + pos = checkpoint; + paramNames = null; + break; + } + } + } + + if (paramNames != null) { + if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + advance(); // consume ')' + if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) { + // Valid lambda! + } else { + // Not a lambda, rewind + pos = checkpoint; + paramNames = null; + } + } else { + // No closing paren, rewind + pos = checkpoint; + paramNames = null; + } + } + } + + if (paramNames != null) { + // This is a lambda! Parse it + advance(); // consume '->' + + // Parse body + ExpressionNode body; + boolean isBlock = false; + int bodyEnd; + + if (check(ExpressionToken.TokenKind.LEFT_BRACE)) { + // Block lambda: { ... } + isBlock = true; + int braceStart = current().getStart(); + advance(); // consume '{' + // Skip to matching '}' + int depth = 1; + while (depth > 0 && pos < tokens.size() && !check(ExpressionToken.TokenKind.EOF)) { + if (check(ExpressionToken.TokenKind.LEFT_BRACE)) depth++; + else if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) depth--; + if (depth > 0) advance(); + } + bodyEnd = current().getEnd(); + if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) { + advance(); // consume '}' + } + body = new ExpressionNode.StringLiteralNode("", braceStart, bodyEnd); // Placeholder + } else { + // Expression lambda + body = parseExpressionInternal(0); + if (body == null) { + // Failed to parse body, return error recovery + return null; + } + bodyEnd = body.getEnd(); + } + + // Create lambda node (scopeRef will be set during type resolution) + return new ExpressionNode.LambdaNode(paramNames, body, isBlock, lambdaStart, bodyEnd); + } + + // Not a lambda, parse as regular expression + return parseExpressionInternal(0); } private ExpressionToken current() { @@ -36,6 +148,15 @@ private boolean match(ExpressionToken.TokenKind kind) { } private ExpressionNode parseExpression(int minPrecedence) { + // For top-level expressions (minPrecedence = 0), check for lambda first + if (minPrecedence == 0) { + return parseLambdaOrExpression(); + } + + return parseExpressionInternal(minPrecedence); + } + + private ExpressionNode parseExpressionInternal(int minPrecedence) { ExpressionNode left = parsePrefixExpression(); if (left == null) return null; @@ -108,6 +229,11 @@ private ExpressionNode parsePrefixExpression() { ExpressionToken tok = current(); int start = tok.getStart(); + // Check for function expression (JS only) + if (tok.getKind() == ExpressionToken.TokenKind.FUNCTION) { + return parseFunctionExpression(); + } + if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) { OperatorType op = tok.getOperatorType(); if (op != null && isUnaryOperator(op)) { @@ -126,6 +252,80 @@ private ExpressionNode parsePrefixExpression() { return parsePrimaryExpression(); } + private ExpressionNode parseFunctionExpression() { + int start = current().getStart(); + advance(); // consume 'function' + + // Optional function name + String functionName = null; + if (check(ExpressionToken.TokenKind.IDENTIFIER)) { + functionName = current().getText(); + advance(); + } + + // Expect '(' + if (!check(ExpressionToken.TokenKind.LEFT_PAREN)) { + return null; // Invalid syntax + } + advance(); // consume '(' + + // Parse parameters + List params = new ArrayList<>(); + while (!check(ExpressionToken.TokenKind.RIGHT_PAREN) && !check(ExpressionToken.TokenKind.EOF)) { + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) { + return null; // Invalid parameter + } + params.add(current().getText()); + advance(); + + if (check(ExpressionToken.TokenKind.COMMA)) { + advance(); + } else if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + return null; // Expected comma or closing paren + } + } + + if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) { + return null; // Expected closing paren + } + advance(); // consume ')' + + // Expect '{' + if (!check(ExpressionToken.TokenKind.LEFT_BRACE)) { + return null; // Invalid syntax + } + advance(); // consume '{' + + // Collect body tokens until matching '}' + StringBuilder bodyBuilder = new StringBuilder(); + int depth = 1; + while (depth > 0 && !check(ExpressionToken.TokenKind.EOF)) { + ExpressionToken bodyToken = current(); + if (bodyToken.getKind() == ExpressionToken.TokenKind.LEFT_BRACE) { + depth++; + } else if (bodyToken.getKind() == ExpressionToken.TokenKind.RIGHT_BRACE) { + depth--; + if (depth == 0) { + break; + } + } + bodyBuilder.append(bodyToken.getText()).append(" "); + advance(); + } + + String bodyText = bodyBuilder.toString().trim(); + int end = current().getEnd(); + + if (!check(ExpressionToken.TokenKind.RIGHT_BRACE)) { + return null; // Expected closing brace + } + advance(); // consume final '}' + + // Find the corresponding InnerCallableScope from ScriptDocument + // This will be set during type resolution when we have access to ScriptDocument + return new ExpressionNode.JSFunctionNode(functionName, params, bodyText, start, end); + } + private boolean isUnaryOperator(OperatorType op) { switch (op) { case ADD: case SUBTRACT: case LOGICAL_NOT: case BITWISE_NOT: @@ -250,7 +450,34 @@ private ExpressionNode parseIdentifierOrMethodCall() { private ExpressionNode parseAccessChain(ExpressionNode base) { while (true) { - if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { + if (check(ExpressionToken.TokenKind.METHOD_REFERENCE)) { + advance(); // consume '::' + + if (!check(ExpressionToken.TokenKind.IDENTIFIER)) { + // Invalid syntax, return what we have + break; + } + + String methodName = current().getText(); + int end = current().getEnd(); + advance(); + + // Determine if this is a static or instance method reference + boolean isStatic = false; + if (base instanceof ExpressionNode.IdentifierNode) { + // Could be Class::method or obj::method + // We'll determine this during type resolution + String targetName = ((ExpressionNode.IdentifierNode) base).getName(); + // If targetName starts with uppercase, likely a class + if (targetName.length() > 0) { + isStatic = Character.isUpperCase(targetName.charAt(0)); + } + } + + base = new ExpressionNode.MethodReferenceNode(base, methodName, isStatic, base.getStart(), end); + // Method references don't chain further (can't do obj::method.something) + break; + } else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { advance(); List args = parseArgumentList(); int end = current().getStart(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java index 655adea9a..37e2c4ae1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionToken.java @@ -5,10 +5,12 @@ public class ExpressionToken { public enum TokenKind { INT_LITERAL, LONG_LITERAL, FLOAT_LITERAL, DOUBLE_LITERAL, BOOLEAN_LITERAL, CHAR_LITERAL, STRING_LITERAL, NULL_LITERAL, - IDENTIFIER, NEW, INSTANCEOF, + IDENTIFIER, NEW, INSTANCEOF, FUNCTION, OPERATOR, LEFT_PAREN, RIGHT_PAREN, LEFT_BRACKET, RIGHT_BRACKET, + LEFT_BRACE, RIGHT_BRACE, DOT, COMMA, QUESTION, COLON, SEMICOLON, + LAMBDA_ARROW, METHOD_REFERENCE, EOF } @@ -55,6 +57,9 @@ public static ExpressionToken identifier(String name, int start, int end) { if ("instanceof".equals(name)) { return new ExpressionToken(TokenKind.INSTANCEOF, name, start, end); } + if ("function".equals(name)) { + return new ExpressionToken(TokenKind.FUNCTION, name, start, end); + } return new ExpressionToken(TokenKind.IDENTIFIER, name, start, end); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java index 25ee10ea5..795344856 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTokenizer.java @@ -98,6 +98,21 @@ public static List tokenize(String expr) { int start = pos; String op = null; + + // Check for method reference :: (before lambda arrow and single colon) + if (pos + 1 < len && c == ':' && expr.charAt(pos + 1) == ':') { + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.METHOD_REFERENCE, "::", pos, pos + 2)); + pos += 2; + continue; + } + + // Check for lambda arrow + if (pos + 1 < len && c == '-' && expr.charAt(pos + 1) == '>') { + tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LAMBDA_ARROW, "->", pos, pos + 2)); + pos += 2; + continue; + } + if (pos + 3 <= len) { String s3 = expr.substring(pos, pos + 3); if (">>>".equals(s3) || "<<=".equals(s3) || ">>=".equals(s3)) op = s3; @@ -133,6 +148,8 @@ public static List tokenize(String expr) { case ')': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_PAREN, ")", pos, pos + 1)); break; case '[': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACKET, "[", pos, pos + 1)); break; case ']': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACKET, "]", pos, pos + 1)); break; + case '{': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACE, "{", pos, pos + 1)); break; + case '}': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACE, "}", pos, pos + 1)); break; case '.': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.DOT, ".", pos, pos + 1)); break; case ',': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COMMA, ",", pos, pos + 1)); break; case '?': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.QUESTION, "?", pos, pos + 1)); break; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java index 86aaacec9..70c744199 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -1,10 +1,16 @@ package noppes.npcs.client.gui.util.script.interpreter.expression; +import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument; +import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope; +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; import java.util.List; public class ExpressionTypeResolver { private final ExpressionNode.TypeResolverContext context; + private final ScriptDocument document; // Access to document for inner scope lookups + private final int basePosition; // Document offset for this expression /** * Static field to track the expected/desired type for the current expression being resolved. @@ -16,6 +22,18 @@ public class ExpressionTypeResolver { public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) { this.context = context; + this.document = null; + this.basePosition = 0; + } + + public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document) { + this(context, document, 0); + } + + public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document, int basePosition) { + this.context = context; + this.document = document; + this.basePosition = basePosition; } public TypeInfo resolve(String expression) { @@ -133,9 +151,272 @@ private TypeInfo resolveNodeType(ExpressionNode node) { return resolveNodeType(((ExpressionNode.ParenthesizedNode) node).getInner()); } + if (node instanceof ExpressionNode.LambdaNode) { + return resolveLambdaType((ExpressionNode.LambdaNode) node); + } + + if (node instanceof ExpressionNode.JSFunctionNode) { + return resolveJSFunctionType((ExpressionNode.JSFunctionNode) node); + } + + if (node instanceof ExpressionNode.MethodReferenceNode) { + return resolveMethodReferenceType((ExpressionNode.MethodReferenceNode) node); + } + + return null; + } + + private TypeInfo resolveLambdaType(ExpressionNode.LambdaNode lambda) { + // Lambda type is determined by the expected functional interface + TypeInfo expectedType = CURRENT_EXPECTED_TYPE; + + if (expectedType == null) { + // No expected type context + // Return a generic Object type + return TypeInfo.fromClass(Object.class); + } + + // Get SAM method from the expected functional interface + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam != null && document != null) { + // Find the InnerCallableScope for this lambda by matching position and parameters + InnerCallableScope scope = findLambdaScopeByPosition(lambda.getStart(), lambda.getParameterNames()); + + if (scope != null) { + scope.setExpectedType(expectedType); + + // Extract and apply parameter types from SAM to lambda parameters + List samParams = sam.getParameters(); + List lambdaParams = scope.getParameters(); + + if (samParams.size() == lambdaParams.size()) { + for (int i = 0; i < lambdaParams.size(); i++) { + FieldInfo lambdaParam = lambdaParams.get(i); + TypeInfo inferredType = samParams.get(i).getTypeInfo(); + + // Set inferred type on the parameter + if (inferredType != null) { + lambdaParam.setInferredType(inferredType); + } + } + } + } + + // Type check lambda body (if expression lambda) + if (!lambda.isBlock() && lambda.getBody() != null) { + // Resolve body type for validation + TypeInfo bodyType = resolveNodeType(lambda.getBody()); + // The actual compatibility check will be done during marking phase + } + + return expectedType; + } + + // Fallback: check if it's a recognized functional interface by name + String typeName = expectedType.getFullName(); + if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") || + typeName.contains("Predicate") || typeName.contains("Supplier") || + typeName.equals("java.lang.Runnable"))) { + // Update the scope reference if available + if (lambda.getScopeRef() != null) { + lambda.getScopeRef().setExpectedType(expectedType); + } + + return expectedType; + } + + // Not a recognized functional interface, return Object + return TypeInfo.fromClass(Object.class); + } + + /** + * Find the InnerCallableScope for a lambda by matching position and parameter names. + * + * @param expressionRelativePos The start position of the lambda (relative to expression string) + * @param paramNames The parameter names of the lambda + * @return The matching InnerCallableScope, or null if not found + */ + private InnerCallableScope findLambdaScopeByPosition(int expressionRelativePos, List paramNames) { + if (document == null) { + return null; + } + + // Convert expression-relative position to absolute document position + int absolutePos = basePosition + expressionRelativePos; + + for (InnerCallableScope scope : document.getInnerScopes()) { + if (scope.getKind() == InnerCallableScope.Kind.JAVA_LAMBDA) { + // Check if absolute position is in range + if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) { + // Verify parameter names match + if (scope.getParameters().size() == paramNames.size()) { + boolean match = true; + for (int i = 0; i < paramNames.size(); i++) { + if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) { + match = false; + break; + } + } + if (match) { + return scope; + } + } + } + } + } return null; } + private TypeInfo resolveJSFunctionType(ExpressionNode.JSFunctionNode functionNode) { + // JS function type is determined by the expected functional interface (if any) + TypeInfo expectedType = CURRENT_EXPECTED_TYPE; + + if (expectedType == null) { + // No expected type - return generic Function type or Object + // Try to resolve "Function" as a type from the context + TypeInfo functionType = context.resolveTypeName("Function"); + if (functionType != null && functionType.isResolved()) { + return functionType; + } + return TypeInfo.fromClass(Object.class); // Fallback + } + + // Get SAM method from the expected functional interface + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam != null && document != null) { + // Find the InnerCallableScope for this JS function by matching position and parameters + InnerCallableScope scope = findJSFunctionScopeByPosition(functionNode.getStart(), functionNode.getParameterNames()); + + if (scope != null) { + scope.setExpectedType(expectedType); + + // Extract and apply parameter types from SAM to function parameters + List samParams = sam.getParameters(); + List functionParams = scope.getParameters(); + + if (samParams.size() == functionParams.size()) { + for (int i = 0; i < functionParams.size(); i++) { + FieldInfo functionParam = functionParams.get(i); + TypeInfo inferredType = samParams.get(i).getTypeInfo(); + + // Set inferred type on the parameter + if (inferredType != null) { + functionParam.setInferredType(inferredType); + } + } + } + } + + return expectedType; + } + + // Fallback: check if expected type is compatible with a function + String typeName = expectedType.getFullName(); + if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") || + typeName.contains("Predicate") || typeName.contains("Supplier") || + typeName.equals("java.lang.Runnable"))) { + // This is likely a functional interface + + // Update the InnerCallableScope with the expected type + if (functionNode.getScopeRef() != null) { + functionNode.getScopeRef().setExpectedType(expectedType); + } + + return expectedType; + } + + // Expected type is not a recognized functional interface + // Check if it's a generic JS "Function" type expectation + if (typeName != null && typeName.equals("Function")) { + return expectedType; + } + + return TypeInfo.fromClass(Object.class); // Fallback + } + + /** + * Find the InnerCallableScope for a JS function by matching position and parameter names. + * + * @param expressionRelativePos The start position of the function (relative to expression string) + * @param paramNames The parameter names of the function + * @return The matching InnerCallableScope, or null if not found + */ + private InnerCallableScope findJSFunctionScopeByPosition(int expressionRelativePos, List paramNames) { + if (document == null) { + return null; + } + + // Convert expression-relative position to absolute document position + int absolutePos = basePosition + expressionRelativePos; + + for (InnerCallableScope scope : document.getInnerScopes()) { + if (scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR) { + // Check if absolute position is in range + if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) { + // Verify parameter names match + if (scope.getParameters().size() == paramNames.size()) { + boolean match = true; + for (int i = 0; i < paramNames.size(); i++) { + if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) { + match = false; + break; + } + } + if (match) { + return scope; + } + } + } + } + } + return null; + } + + private TypeInfo resolveMethodReferenceType(ExpressionNode.MethodReferenceNode methodRef) { + // Method reference type is determined by the expected functional interface + TypeInfo expectedType = CURRENT_EXPECTED_TYPE; + + if (expectedType == null) { + // No expected functional interface context + return TypeInfo.fromClass(Object.class); // Fallback + } + + // Check if expected type is a functional interface + if (!isFunctionalInterface(expectedType)) { + // Not a functional interface + return TypeInfo.fromClass(Object.class); + } + + // For now, we'll do basic validation and return the expected type + // In a full implementation, we would: + // 1. Resolve the target type (left side of ::) + // 2. Find the referenced method on the target type + // 3. Extract SAM signature from expected type + // 4. Validate parameter/return type compatibility + + // Basic implementation: assume valid and return expected type + return expectedType; + } + + private boolean isFunctionalInterface(TypeInfo type) { + // Check if the type has exactly one abstract method (SAM type) + // Common functional interfaces: Runnable, Supplier, Consumer, Function, Predicate, etc. + if (type == null) return false; + + String typeName = type.getFullName(); + if (typeName == null) return false; + + return typeName.equals("java.lang.Runnable") || + typeName.contains("Supplier") || + typeName.contains("Consumer") || + typeName.contains("Function") || + typeName.contains("Predicate") || + typeName.contains("BiFunction") || + typeName.contains("BiConsumer") || + typeName.contains("UnaryOperator") || + typeName.contains("BinaryOperator"); + } + public static ExpressionNode.TypeResolverContext createBasicContext() { return new ExpressionNode.TypeResolverContext() { public TypeInfo resolveIdentifier(String name) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java index 2c021920a..8648d91f9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/field/FieldInfo.java @@ -509,7 +509,11 @@ public TypeInfo getTypeInfo() { return getEffectiveType(); } public int getDeclarationOffset() { return declarationOffset; } - public boolean isResolved() { return resolved; } + public boolean isResolved() { + // Check if we have any resolved type (declared or inferred) + TypeInfo effectiveType = getEffectiveType(); + return effectiveType != null && effectiveType.isResolved(); + } public MethodInfo getContainingMethod() { return containingMethod; } public String getDocumentation() { return documentation; } public JSDocInfo getJSDocInfo() { return jsDocInfo; } From 9e4692d8e430eae5698dbbc2c0e21a37c3f1d94d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 07:04:50 +0200 Subject: [PATCH 321/337] Support d.ts patch overlays in gradle plugin GenerateTypeScriptTask can now copy an optional patches directory into the generated output so manual overrides can ship alongside generated defs. Converter tweaks: anchor method signature regex to line starts, and ensure generated (including nested) types include a @javaFqn tag for reliable mapping back to Java sources. --- gradle-plugins/.gitignore | 5 +- gradle-plugins/README.md | 25 +++++++++ .../groovy/dts/GenerateTypeScriptTask.groovy | 40 ++++++++++++- .../dts/JavaToTypeScriptConverter.groovy | 56 ++++++++++++++----- 4 files changed, 111 insertions(+), 15 deletions(-) diff --git a/gradle-plugins/.gitignore b/gradle-plugins/.gitignore index 94cac27cf..22bff6717 100644 --- a/gradle-plugins/.gitignore +++ b/gradle-plugins/.gitignore @@ -3,4 +3,7 @@ /out /bin /.idea -/.vscode \ No newline at end of file +/.vscode +/.settings +.project +.classpath \ No newline at end of file diff --git a/gradle-plugins/README.md b/gradle-plugins/README.md index 78f1a1091..1e0e7fba2 100644 --- a/gradle-plugins/README.md +++ b/gradle-plugins/README.md @@ -60,6 +60,9 @@ tasks.named("generateTypeScriptDefinitions").configure { // Whether to clean old generated files before regenerating cleanOutputFirst = true + + // Optional: copy external patch .d.ts files into assets//api/patches + patchesDirectory = "dts-patches" } @@ -67,3 +70,25 @@ tasks.named("generateTypeScriptDefinitions").configure { // But in most cases, you may want to run the task manually when needed // processResources.dependsOn generateTypeScriptDefinitions ``` + +--- + +## Patches (optional overrides) + +Use `dts-patches/` to override or refine generated types. Files in this folder are copied to +`assets//api/patches` after generation. + +### Example: override `IPlayer.getDBCPlayer()` to return `IDBCAddon` instead of `IDBCPlayer` + +File: `dts-patches/IPlayer.d.ts` + +```ts +/** + * DBC Addon patch for IPlayer + * @javaFqn noppes.npcs.api.entity.IPlayer + */ +export interface IPlayer { + getDBCPlayer(): IDBCAddon; +} +``` + diff --git a/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy b/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy index 31a06c609..b62e6cedc 100644 --- a/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy +++ b/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy @@ -3,11 +3,11 @@ package dts import org.gradle.api.DefaultTask import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Optional -import org.gradle.api.file.FileCollection /** * Gradle task that generates TypeScript definition (.d.ts) files from Java API sources. @@ -37,6 +37,9 @@ abstract class GenerateTypeScriptTask extends DefaultTask { @Input List excludePatterns = [] + + @Internal + Object patchesDirectory // Provide task input/output properties that Gradle can validate @InputFiles @@ -71,6 +74,24 @@ abstract class GenerateTypeScriptTask extends DefaultTask { } return f } + + @Optional + @InputDirectory + protected File getResolvedDtsPatchesDirectory() { + if (patchesDirectory == null) return null + def obj = patchesDirectory + if (obj instanceof File) return obj + String pathStr = obj.toString() + if (pathStr.contains('${modid}')) { + String modid = project.archivesBaseName ?: 'mod' + pathStr = pathStr.replace('${modid}', modid) + } + File f = new File(pathStr) + if (!f.isAbsolute()) { + f = new File(project.projectDir, pathStr) + } + return f + } /** * Converts a value (String or File) to a File object. @@ -87,6 +108,7 @@ abstract class GenerateTypeScriptTask extends DefaultTask { void generate() { List srcDirs = getResolvedSourceDirectories() File outDir = getResolvedOutputDirectory() + File patchesDir = getResolvedDtsPatchesDirectory() logger.lifecycle("=".multiply(60)) logger.lifecycle("Generating TypeScript definitions...") @@ -129,6 +151,22 @@ abstract class GenerateTypeScriptTask extends DefaultTask { } converter.processDirectories(validDirs, logger) + + if (patchesDir != null) { + if (!patchesDir.exists()) { + logger.lifecycle("Patches directory does not exist: ${patchesDir}") + } else { + File patchOutputDir = new File(outDir, "patches") + if (patchOutputDir.exists()) { + patchOutputDir.deleteDir() + } + project.copy { + from patchesDir + into patchOutputDir + } + logger.lifecycle("Copied .d.ts patches to: ${patchOutputDir}") + } + } logger.lifecycle("=".multiply(60)) logger.lifecycle("TypeScript definition generation complete!") diff --git a/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy b/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy index 16d71bc90..9d96633ed 100644 --- a/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy +++ b/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy @@ -334,8 +334,9 @@ class JavaToTypeScriptConverter { String topLevelBody = removeNestedTypeBodies(body) // Match method signatures - handles complex generics + // Anchored with (?m)^ to prevent matching inside // comment lines // Capture JSDoc in group 1, returnType in group 2, methodName in group 3, params in group 4 - def methodPattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:abstract\s+)?(?:default\s+)?(?:synchronized\s+)?(?:final\s+)?(?:<[^>]+>\s+)?(\w[\w.<>,\[\]\s]*?)\s+(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?[;{]/ + def methodPattern = ~/(?m)^\s*(\/\*\*[\s\S]*?\*\/\s*)?(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:abstract\s+)?(?:default\s+)?(?:synchronized\s+)?(?:final\s+)?(?:<[^>]+>\s+)?(\w[\w.<>,\[\]\s]*?)\s+(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?[;{]/ def matcher = topLevelBody =~ methodPattern while (matcher.find()) { @@ -542,8 +543,10 @@ class JavaToTypeScriptConverter { Set typeParamNames = type.parsedTypeParams?.collect { it.name }?.toSet() ?: [] as Set // JSDoc - if (type.jsdoc) { - sb.append(convertJsDoc(type.jsdoc, indent)) + String javaFqn = buildJavaFqn(parsed.packageName, type.name) + String typeJsDoc = ensureJavaFqnTag(type.jsdoc, javaFqn, indent) + if (typeJsDoc) { + sb.append(typeJsDoc) sb.append('\n') } @@ -581,7 +584,7 @@ class JavaToTypeScriptConverter { type.nestedTypes.each { nested -> // Always generate as interface, even if empty // Empty interfaces with extends are important for type hierarchy - generateNestedType(sb, nested, type.name, parsed, currentPath, indent + ' ') + generateNestedType(sb, nested, type.name, type.name, parsed, currentPath, indent + ' ') } sb.append("${indent}}\n") } @@ -592,13 +595,16 @@ class JavaToTypeScriptConverter { * Generate a nested type (interface/class within a namespace) * Recursively handles nested types that themselves have nested types */ - private void generateNestedType(StringBuilder sb, JavaType type, String parentTypeName, ParsedJavaFile parsed, String currentPath, String indent) { + private void generateNestedType(StringBuilder sb, JavaType type, String parentTypeName, String parentTypeChain, ParsedJavaFile parsed, String currentPath, String indent) { // Build a set of type parameter names for this nested type Set typeParamNames = type.parsedTypeParams?.collect { it.name }?.toSet() ?: [] as Set // JSDoc - if (type.jsdoc) { - sb.append(convertJsDoc(type.jsdoc, indent)) + String nestedChain = parentTypeChain ? "${parentTypeChain}.${type.name}" : type.name + String javaFqn = buildJavaFqn(parsed.packageName, nestedChain) + String nestedJsDoc = ensureJavaFqnTag(type.jsdoc, javaFqn, indent) + if (nestedJsDoc) { + sb.append(nestedJsDoc) sb.append('\n') } @@ -632,7 +638,7 @@ class JavaToTypeScriptConverter { type.nestedTypes.each { nested -> // Always generate as interface, even if empty // Empty interfaces with extends are important for type hierarchy - generateNestedType(sb, nested, type.name, parsed, currentPath, indent + ' ') + generateNestedType(sb, nested, type.name, nestedChain, parsed, currentPath, indent + ' ') } sb.append("${indent}}\n") } @@ -717,11 +723,6 @@ class JavaToTypeScriptConverter { return convertGenericType(javaType, parsed, currentPath, typeParamNames) } - // Handle functional interfaces from java.util.function - if (FUNCTIONAL_MAPPINGS.containsKey(javaType)) { - return 'Function' // Simplified - will be expanded in generic handling - } - // Check if it is an API type (needs import) String importPath = resolveImportPath(javaType, parsed, currentPath) if (importPath != null) { @@ -1005,6 +1006,35 @@ class JavaToTypeScriptConverter { } }.join('\n') } + + private String ensureJavaFqnTag(String jsdoc, String javaFqn, String indent) { + if (javaFqn == null || javaFqn.isEmpty()) { + return convertJsDoc(jsdoc, indent) + } + + if (jsdoc != null && jsdoc.contains('@javaFqn')) { + return convertJsDoc(jsdoc, indent) + } + + if (jsdoc == null || jsdoc.trim().isEmpty()) { + return "${indent}/**\n${indent} * @javaFqn ${javaFqn}\n${indent} */" + } + + String converted = convertJsDoc(jsdoc, indent) + int endIndex = converted.lastIndexOf('*/') + if (endIndex >= 0) { + String insert = "${indent} * @javaFqn ${javaFqn}\n" + return converted.substring(0, endIndex) + insert + converted.substring(endIndex) + } + + return converted + "\n${indent} * @javaFqn ${javaFqn}" + } + + private String buildJavaFqn(String packageName, String typeName) { + if (typeName == null || typeName.isEmpty()) return null + if (packageName == null || packageName.isEmpty()) return typeName + return packageName + '.' + typeName + } /** * Extract @hookName value from a JSDoc comment. From 9db8a0b2e75fab801a4a61b3f7d851c9d35892d5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 07:20:24 +0200 Subject: [PATCH 322/337] Forgotten Merge resolution for 'Add Tooltips.' commit --- .../java/noppes/npcs/client/gui/util/GuiNPCInterface.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 0e53ba30d..f417f854a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -410,8 +410,8 @@ public void drawScreen(int i, int j, float f) { } for (GuiNpcTextField tf : textfields.values()) { tf.drawTextBox(i, j); - if (textField.hasHoverText()) - textField.drawHover(i, j, subGui); + if (tf.hasHoverText()) + tf.drawHover(i, j, subGui); } for (GuiScreen gui : extra.values()) From 6fc0bf4d6b17c1bf6e9d9899eeaf512c02404046 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 22:12:43 +0200 Subject: [PATCH 323/337] Fix expression typing gate to route JS function/lambda expressions into ExpressionParser Problem: - function(act){...} expressions not reaching ExpressionParser - resolveExpressionType only checked containsOperators() or startsWith("(") - Function expressions went through simple resolver and returned null type Solution: - Added looksLikeFunctionOrLambda helper (lines 4766-4773) to detect: * JS functions: function(...) * Arrow functions: => * Java lambdas: -> - Updated gate condition at line 4144 to route these into ExpressionParser - Function expressions now reach ExpressionTypeResolver.resolveJSFunctionType Impact: - SAM typing now works in pass-2 when expected type is known - Fixes schedule("TpGainAction", function(act){...}) argument typing - Enables proper overload resolution for function/lambda parameters --- .../script/interpreter/ScriptDocument.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) 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 e7dd78a6f..e25a0ccdd 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 @@ -4138,11 +4138,12 @@ public TypeInfo resolveExpressionType(String expr, int position) { return null; } - // Check if expression contains operators - if so, use the full expression resolver - // Handle cast expressions: (Type)expr, ((Type)expr).method(), etc. - if (containsOperators(expr) || expr.startsWith("(")) { - return resolveExpressionWithParserAPI(expr, position); - } + // Check if expression contains operators - if so, use the full expression resolver + // Handle cast expressions: (Type)expr, ((Type)expr).method(), etc. + // Also route JS function expressions and arrow lambdas through the parser + if (containsOperators(expr) || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) { + return resolveExpressionWithParserAPI(expr, position); + } // Invalid expressions starting with brackets if (expr.startsWith("[") || expr.startsWith("]")) { @@ -4755,6 +4756,22 @@ public boolean containsOperators(String expr) { return false; } + /** + * Check if an expression looks like a JS function or arrow lambda. + * This is a fast heuristic to detect: + * - JS function expressions: function(...) {} + * - Arrow lambdas: (...) => ... + * These need to be routed through the parser for proper SAM typing. + */ + private boolean looksLikeFunctionOrLambda(String expr) { + if (expr == null || expr.isEmpty()) return false; + String trimmed = expr.trim(); + if (trimmed.startsWith("function")) return true; + if (trimmed.contains("=>")) return true; + if (trimmed.contains("->")) return true; + return false; + } + /** * Resolve an expression that contains operators or casts using the full expression parser. * This handles all Java operators with proper precedence and type promotion rules. From 7f4bc87d7a40014429f3874cb4204118fe74fc3c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 3 Feb 2026 22:13:15 +0200 Subject: [PATCH 324/337] Extract overload selection to OverloadSelector helper with scoring algorithm Problem: - schedule("TpGainAction", function(act){...}) resolved to wrong overload - TypeInfo.getBestMethodOverload rejected candidates when argType == null - Fell back to arbitrary overloads.get(0) based on reflection order - Result: selected 1-arg varargs overload instead of 2-arg fixed-arity Solution: - Created OverloadSelector helper class implementing scoring algorithm - Scoring tiers (lower is better): * Arity mismatch: +10000 + distance penalty (hard disqualifier) * Varargs: +1000 penalty (fixed-arity strongly preferred) * Unknown arg type (null): +10 (penalty but doesn't disqualify) * Compatible type: +5 (modest preference for matching) * Exact match: +0 (best case) - Closest arity fallback when no arity-applicable overload exists - TypeInfo.getBestMethodOverload now delegates (4 lines vs 220) Impact: - Deterministic overload selection (no reflection-order dependence) - schedule("TpGainAction", function(act){...}) now resolves correctly - Unknown arg types no longer cause fallback to unrelated overloads - Fixed-arity methods preferred over varargs when both viable - Removed 220 lines of complex nested logic from TypeInfo Testing: - OverloadResolutionTest with 6 test cases (all passing) - Verifies fixed-arity preference, unknown arg handling, arity fallback --- .../interpreter/type/OverloadSelector.java | 296 ++++++++++++++++++ .../script/interpreter/type/TypeInfo.java | 109 +------ 2 files changed, 297 insertions(+), 108 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/OverloadSelector.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/OverloadSelector.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/OverloadSelector.java new file mode 100644 index 000000000..4bd105aa8 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/OverloadSelector.java @@ -0,0 +1,296 @@ +package noppes.npcs.client.gui.util.script.interpreter.type; + +import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; +import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; + +import java.util.List; + +/** + * Helper class for selecting the best method overload from a list of candidates. + * + * This encapsulates the scoring-based overload selection logic that considers: + * - Exact type matches + * - Numeric promotion compatibility + * - Arity (parameter count) matching + * - Varargs handling + * - Unknown (null) argument types + * + * Lower scores indicate better matches. + */ +public class OverloadSelector { + + // Scoring constants + private static final int ARITY_MISMATCH_BASE = 10000; + private static final int VARARGS_PENALTY = 1000; + private static final int UNKNOWN_TYPE_PENALTY = 10; + private static final int COMPATIBLE_TYPE_PENALTY = 5; + private static final int INCOMPATIBLE_TYPE_PENALTY = 100; + + /** + * Select the best overload from a list of candidates based on argument types. + * + * Selection phases: + * 1. Exact match (all types match exactly) + * 2. Numeric promotion match (all args can promote to common param type) + * 3. Scoring-based selection (handles unknown types, varargs, etc.) + * + * @param overloads List of candidate method overloads + * @param argTypes Array of argument types (may contain nulls for unknown types) + * @return The best matching overload, or the first overload if no good match found + */ + public static MethodInfo selectBestOverload(List overloads, TypeInfo[] argTypes) { + if (overloads == null || overloads.isEmpty()) return null; + + int argCount = (argTypes == null) ? 0 : argTypes.length; + + // Fast path: If no arguments provided, try to find zero-arg method + if (argCount == 0) { + for (MethodInfo method : overloads) { + if (method.getParameterCount() == 0) { + return method; + } + } + // Fall back to closest arity + return selectClosestArity(overloads, 0); + } + + // Check if all arg types are known (non-null) + boolean allArgsKnown = true; + for (TypeInfo argType : argTypes) { + if (argType == null) { + allArgsKnown = false; + break; + } + } + + // Phase 1: Try exact match (only when all args known) + if (allArgsKnown) { + MethodInfo exactMatch = findExactMatch(overloads, argTypes, argCount); + if (exactMatch != null) return exactMatch; + } + + // Phase 2: Try numeric promotion match (only when all args known) + if (allArgsKnown) { + MethodInfo numericMatch = findNumericPromotionMatch(overloads, argTypes, argCount); + if (numericMatch != null) return numericMatch; + } + + // Phase 3: Scoring-based selection with arity-first preference + // This handles unknown (null) arg types gracefully and prefers fixed-arity over varargs + return scoringBasedSelection(overloads, argTypes, argCount); + } + + /** + * Find an overload with exact type match for all arguments. + */ + private static MethodInfo findExactMatch(List overloads, TypeInfo[] argTypes, int argCount) { + for (MethodInfo method : overloads) { + if (method.getParameterCount() == argCount) { + boolean exactMatch = true; + List params = method.getParameters(); + for (int i = 0; i < argCount; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = argTypes[i]; + if (paramType == null || !paramType.equals(argType)) { + exactMatch = false; + break; + } + } + if (exactMatch) return method; + } + } + return null; + } + + /** + * Find an overload where all numeric arguments can promote to a common parameter type. + * Prefers narrower numeric types (e.g., int over long over double). + */ + private static MethodInfo findNumericPromotionMatch(List overloads, TypeInfo[] argTypes, int argCount) { + MethodInfo bestNumericMatch = null; + int bestNumericRank = Integer.MAX_VALUE; + + for (MethodInfo method : overloads) { + if (method.getParameterCount() == argCount) { + List params = method.getParameters(); + + // Check if all parameters and arguments are numeric primitives + boolean allNumeric = true; + for (int i = 0; i < argCount; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = argTypes[i]; + if (paramType == null || + !TypeChecker.isNumericPrimitive(paramType) || !TypeChecker.isNumericPrimitive(argType)) { + allNumeric = false; + break; + } + } + + if (allNumeric) { + // Check if all parameters are the same numeric type + TypeInfo commonParamType = params.get(0).getTypeInfo(); + boolean allParamsSame = true; + for (int i = 1; i < params.size(); i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + if (!paramType.equals(commonParamType)) { + allParamsSame = false; + break; + } + } + + // If all parameters are the same numeric type, check if args can promote to it + if (allParamsSame) { + boolean canPromote = true; + for (int i = 0; i < argCount; i++) { + if (!TypeChecker.canPromoteNumeric(argTypes[i], commonParamType)) { + canPromote = false; + break; + } + } + + // If all args can promote, check if this is the narrowest match so far + if (canPromote) { + int paramRank = TypeChecker.getNumericRank(commonParamType.getJavaClass()); + if (paramRank < bestNumericRank) { + bestNumericRank = paramRank; + bestNumericMatch = method; + } + } + } + } + } + } + + return bestNumericMatch; + } + + /** + * Use scoring to select the best overload when exact/numeric matches don't apply. + */ + private static MethodInfo scoringBasedSelection(List overloads, TypeInfo[] argTypes, int argCount) { + MethodInfo bestCandidate = null; + int bestScore = Integer.MAX_VALUE; + + for (MethodInfo method : overloads) { + int score = scoreOverload(method, argTypes, argCount); + if (score < bestScore) { + bestScore = score; + bestCandidate = method; + } + } + + return (bestCandidate != null) ? bestCandidate : overloads.get(0); + } + + /** + * Score an overload for matching against provided arguments. + * Lower score = better match. + * + * Scoring priorities: + * - Arity mismatch: +10000 + |paramCount - argCount| + * - Varargs penalty: +1000 + * - Unknown arg type (null): +10 per arg + * - Compatible but not exact: +5 per arg + * - Incompatible type: +100 per arg + * - Exact type match: +0 per arg + * + * @param method The method overload to score + * @param argTypes Array of argument types (may contain nulls) + * @param argCount Number of arguments + * @return Score (lower is better) + */ + public static int scoreOverload(MethodInfo method, TypeInfo[] argTypes, int argCount) { + int score = 0; + int paramCount = method.getParameterCount(); + + // Check if method is varargs + boolean isVarArgs = false; + java.lang.reflect.Method javaMethod = method.getJavaMethod(); + if (javaMethod != null) { + isVarArgs = javaMethod.isVarArgs(); + } + + // Determine arity applicability + boolean arityApplicable; + if (isVarArgs) { + // Varargs methods accept argCount >= paramCount - 1 + // (the varargs array can be empty or have multiple elements) + arityApplicable = argCount >= paramCount - 1; + } else { + arityApplicable = paramCount == argCount; + } + + if (!arityApplicable) { + // Large penalty for arity mismatch, plus distance to encourage closest match + score += ARITY_MISMATCH_BASE + Math.abs(paramCount - argCount); + } + + // Varargs penalty (fixed-arity preferred over varargs when both apply) + if (isVarArgs) { + score += VARARGS_PENALTY; + } + + // Score each argument's type compatibility + List params = method.getParameters(); + int paramsToCheck = Math.min(argCount, paramCount); + + for (int i = 0; i < paramsToCheck; i++) { + TypeInfo paramType = params.get(i).getTypeInfo(); + TypeInfo argType = (argTypes != null && i < argTypes.length) ? argTypes[i] : null; + + if (argType == null) { + // Unknown arg type - don't disqualify, but add penalty + score += UNKNOWN_TYPE_PENALTY; + } else if (paramType == null) { + // Unknown param type - add penalty + score += UNKNOWN_TYPE_PENALTY; + } else if (paramType.equals(argType)) { + // Exact match - no penalty + score += 0; + } else if (TypeChecker.isTypeCompatible(paramType, argType)) { + // Compatible but not exact + score += COMPATIBLE_TYPE_PENALTY; + } else { + // Incompatible type + score += INCOMPATIBLE_TYPE_PENALTY; + } + } + + // Penalize extra args that don't match params (for non-varargs) + if (!isVarArgs && argCount > paramCount) { + score += (argCount - paramCount) * INCOMPATIBLE_TYPE_PENALTY; + } + + return score; + } + + /** + * Select the overload with closest arity to the target. + * Tie-breaker: lower parameter count wins. + * + * @param overloads List of candidate overloads + * @param targetArity The desired number of parameters + * @return The overload with closest arity, or first overload if list is empty + */ + public static MethodInfo selectClosestArity(List overloads, int targetArity) { + if (overloads == null || overloads.isEmpty()) return null; + + MethodInfo best = null; + int bestDistance = Integer.MAX_VALUE; + int bestParamCount = Integer.MAX_VALUE; + + for (MethodInfo method : overloads) { + int paramCount = method.getParameterCount(); + int distance = Math.abs(paramCount - targetArity); + + // Prefer smaller distance; tie-break by lower paramCount + if (distance < bestDistance || (distance == bestDistance && paramCount < bestParamCount)) { + best = method; + bestDistance = distance; + bestParamCount = paramCount; + } + } + + return (best != null) ? best : overloads.get(0); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java index ddfcadb1e..0c5fcf2e0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeInfo.java @@ -795,114 +795,7 @@ public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedRetu public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) { java.util.List overloads = getAllMethodOverloads(methodName); if (overloads.isEmpty()) return null; - - // If no arguments provided, try to find zero-arg method - if (argTypes == null || argTypes.length == 0) { - for (MethodInfo method : overloads) { - if (method.getParameterCount() == 0) { - return method; - } - } - // Fall back to first overload if no zero-arg found - return overloads.get(0); - } - - // Phase 1: Try exact match - for (MethodInfo method : overloads) { - if (method.getParameterCount() == argTypes.length) { - boolean exactMatch = true; - java.util.List params = method.getParameters(); - for (int i = 0; i < argTypes.length; i++) { - TypeInfo paramType = params.get(i).getTypeInfo(); - TypeInfo argType = argTypes[i]; - if (paramType == null || argType == null || !paramType.equals(argType)) { - exactMatch = false; - break; - } - } - if (exactMatch) return method; - } - } - - // Phase 2: Try numeric promotion match - // For methods with all numeric parameters, find the narrowest common type that all args can promote to - MethodInfo bestNumericMatch = null; - int bestNumericRank = Integer.MAX_VALUE; - - for (MethodInfo method : overloads) { - if (method.getParameterCount() == argTypes.length) { - java.util.List params = method.getParameters(); - - // Check if all parameters and arguments are numeric primitives - boolean allNumeric = true; - for (int i = 0; i < argTypes.length; i++) { - TypeInfo paramType = params.get(i).getTypeInfo(); - TypeInfo argType = argTypes[i]; - if (paramType == null || argType == null || - !TypeChecker.isNumericPrimitive(paramType) || !TypeChecker.isNumericPrimitive(argType)) { - allNumeric = false; - break; - } - } - - if (allNumeric) { - // Check if all parameters are the same numeric type - TypeInfo commonParamType = params.get(0).getTypeInfo(); - boolean allParamsSame = true; - for (int i = 1; i < params.size(); i++) { - TypeInfo paramType = params.get(i).getTypeInfo(); - if (!paramType.equals(commonParamType)) { - allParamsSame = false; - break; - } - } - - // If all parameters are the same numeric type, check if args can promote to it - if (allParamsSame) { - boolean canPromote = true; - for (int i = 0; i < argTypes.length; i++) { - if (!TypeChecker.canPromoteNumeric(argTypes[i], commonParamType)) { - canPromote = false; - break; - } - } - - // If all args can promote, check if this is the narrowest match so far - if (canPromote) { - int paramRank = TypeChecker.getNumericRank(commonParamType.getJavaClass()); - if (paramRank < bestNumericRank) { - bestNumericRank = paramRank; - bestNumericMatch = method; - } - } - } - } - } - } - - if (bestNumericMatch != null) { - return bestNumericMatch; - } - - // Phase 3: Try compatible match (widening, autoboxing, subtyping) - for (MethodInfo method : overloads) { - if (method.getParameterCount() == argTypes.length) { - boolean compatible = true; - java.util.List params = method.getParameters(); - for (int i = 0; i < argTypes.length; i++) { - TypeInfo paramType = params.get(i).getTypeInfo(); - TypeInfo argType = argTypes[i]; - if (paramType == null || argType == null || !TypeChecker.isTypeCompatible(paramType, argType)) { - compatible = false; - break; - } - } - if (compatible) return method; - } - } - - // Phase 4: Return first overload as fallback - return overloads.get(0); + return OverloadSelector.selectBestOverload(overloads, argTypes); } /** From df578be0b5aceb66ed3c0cc75703637c7b595581 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 4 Feb 2026 05:26:24 +0200 Subject: [PATCH 325/337] wtf who added this --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 908eae39d..4d121433c 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ run src/main/resources/assets/customnpcs/api df/ tmp/ +src/test + From d2713b48cc078be4a8e587946d4abc1a392c6025 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 4 Feb 2026 05:27:45 +0200 Subject: [PATCH 326/337] Generated API .d.ts files on processResources task --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index f7cec4d4d..89da14cd2 100644 --- a/build.gradle +++ b/build.gradle @@ -54,4 +54,5 @@ tasks.named("generateTypeScriptDefinitions").configure { apiPackages = ['noppes.npcs.api', 'net.minecraft'] as Set cleanOutputFirst = true // Clean old generated files before regenerating } +processResources.dependsOn generateTypeScriptDefinitions From 0be0508c18593bb169e10eedb164cd1cf029ad40 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 4 Feb 2026 18:06:49 -0500 Subject: [PATCH 327/337] Update build.gradle --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 8f06d231d..a05d5d7a5 100644 --- a/build.gradle +++ b/build.gradle @@ -70,4 +70,5 @@ tasks.named("generateTypeScriptDefinitions").configure { cleanOutputFirst = true // Clean old generated files before regenerating } processResources.dependsOn generateTypeScriptDefinitions +sourcesJar.dependsOn generateTypeScriptDefinitions From ba23b9f6508f2a7e8261e7f48e0825437fc8b48e Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 4 Feb 2026 18:37:23 -0500 Subject: [PATCH 328/337] 1.11 Update Highlighting --- 1.11_Beta_Update.md | 92 ++++++++++++++++++++++++++++++++++ 1.11_Update.md | 118 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) create mode 100644 1.11_Beta_Update.md create mode 100644 1.11_Update.md diff --git a/1.11_Beta_Update.md b/1.11_Beta_Update.md new file mode 100644 index 000000000..815c62e5d --- /dev/null +++ b/1.11_Beta_Update.md @@ -0,0 +1,92 @@ +# CNPC+ 1.11 Beta - The Scripting Update + +🟡 **In Testing** + + + +## Features: ⚒️ + +- **Syntax Highlighting:** + +> Full color-coded script editor. Keywords, types, variables, methods, strings, and comments are all distinct. Scope-aware coloring distinguishes local fields from globals and chained method calls. + + + +- **Autocomplete:** + +> Smart suggestions as you type. Context-aware for static vs instance access, shows method parameters, resolves chained calls, and suggests auto imports for classes. Frequently used members prioritized. + + + +- **Error Detection:** + +> Catch mistakes before running. Type mismatches, wrong arguments, missing methods, unused imports, duplicate definitions, and more -- all underlined with hover messages explaining the issue. + + + +- **Hover Information:** + +> Hover any symbol to see its type, documentation, and declaration. Works on methods, fields, classes, and even generics. Click to pin tooltips in place. + + + +- **Script Editor Overhaul:** + +> - **Line Numbers** - Gutter with current line highlighting. +> - **Go To Line** - CTRL+G to jump to any line. +> - **Search & Replace** - CTRL+F / CTRL+H with real-time highlighting. +> - **Go To Definition** - CTRL+Click to jump to any symbol. +> - **Smart Undo/Redo** - Word-level instead of character-level. +> - **Line Operations** - Duplicate, delete, move up/down. +> - **Comment Toggle** - CTRL+/ for selected lines. +> - **Rename** - Rename a symbol across all occurrences. +> - **Smooth Scrolling** - Eased scroll animation. +> - **Indent Guides** - Scope-highlighted vertical indent lines. +> - **Brace Matching** - Highlighted pairs, red for unclosed. +> - **Smart Brackets** - Auto-pairing for `{}`, `()`, `[]`, `""`, `''`. +> - **Fullscreen Mode** - Expand editor to fill the game window. +> - **Shortcut Overlay** - View all keyboard shortcuts at a glance. + + + +- **Java Scripting (Janino):** + +> Write scripts in real Java compiled at runtime. Name methods after hooks for automatic wiring. Language selector per tab to switch between Java and JavaScript. External `.java` file support. + + + +- **Client-Side Scripting:** + +> Run scripts on the client for responsive experiences. Server-controlled -- off by default, must be explicitly enabled. Script files sync automatically on login and reload. + + + +- **Cloner Tab Overhaul:** + +> Folder-based organization for saved NPCs, items, and entries. Full-width directory browser for large collections. Improved tab navigation. + + + +- **Addon API Support:** + +> Addons can ship type definitions that load into the script editor automatically. Full autocomplete, hover info, and documentation for addon APIs. Addons can extend existing CNPC+ types with new methods. + + + +- **Animation Improvements:** + +> Data store for passing data across animation events and frames. New consumer-based task system for animation lifecycle. Expanded scripting API for animation control. + + + +**Extras** + +- Player Dialog Events now work like NPC Dialog Events. +- Attack speed configuration for linked items. +- Client-side balance prediction during Trader interactions. +- Right-click cycling on multi-option NPC buttons (Hair, Eyes, Fur, etc.). +- Fixed quest cooldown for MC Custom and RL Custom timer types. +- Fixed Bard music restarting when opening dialogs. +- Fixed NPC biome spawn settings not saving after editing. +- Fixed block waypoint issues. +- Fixed script config syncing between server and client. diff --git a/1.11_Update.md b/1.11_Update.md new file mode 100644 index 000000000..c6101a559 --- /dev/null +++ b/1.11_Update.md @@ -0,0 +1,118 @@ +# ✏️ CustomNPC+ 1.11 - The Scripting Update ✏️ + +--- + +The **Scripting Update** completely transforms how you write scripts in CNPC+! A brand new **Script Editor** with **syntax highlighting**, **autocomplete**, and **error detection** makes scripting feel like a real IDE. Write scripts in **Java** with the new Janino engine, enable **client-side scripting** for responsive experiences, and organize your Cloner with **folders**! + +--- + +## Script Editor 📝 + +The script editor has been rebuilt from the ground up into a **full-featured code editor** right inside Minecraft. + +### Writing Code + +- **Syntax Highlighting** - Keywords, types, variables, methods, strings, and comments are all color-coded automatically. The editor understands your code -- local variables, global fields, and chained method calls each get their own color +- **Autocomplete** - Start typing and get smart suggestions for methods, fields, and classes. Suggestions know whether you're in a static or instance context, and frequently used members appear first +- **Auto Imports** - Type a class name and the editor suggests the correct import for you +- **Error Detection** - Catch mistakes before you even run your script! Wrong argument types, missing methods, type mismatches, unused imports, and more are all underlined with detailed error messages on hover + +> Hover over any symbol to see its **full type information**, documentation, and declaration -- just like a desktop IDE. + +--- + +### Navigation + +- **Go To Line** - `CTRL+G` jumps to any line instantly +- **Search & Replace** - `CTRL+F` to search, `CTRL+H` to replace. All matches highlight in real-time +- **Go To Definition** - `CTRL+Click` any method, field, or type to jump straight to its definition +- **Fullscreen Mode** - Expand the editor to fill your entire game window for maximum workspace + +### Editing + +- **Undo/Redo** - Smart undo that works on whole words at a time, not individual characters +- **Line Operations** - Duplicate lines with `CTRL+D`, delete lines, and move lines up or down with shortcuts +- **Comment Toggle** - `CTRL+/` to comment or uncomment selected lines +- **Rename** - Rename a variable or method and every occurrence updates at once +- **Copy Whole Line** - Copy with nothing selected grabs the entire line +- **Triple Click** - Select an entire line instantly + +### Visual Polish + +- **Line Numbers** - A clean gutter with line numbers and current line highlighting +- **Smooth Scrolling** - Eased scroll animations for a polished feel +- **Indent Guides** - Vertical lines show your code's nesting depth, with the current scope highlighted in green +- **Brace Matching** - Matching `{}` braces light up, and unclosed braces turn red +- **Smart Brackets** - Typing `{` automatically creates the closing `}` with correct indentation. Same for `()`, `[]`, `""`, and `''` +- **Keyboard Shortcut Overlay** - Press a button to see all available shortcuts at a glance + +--- + +## Java Scripting ☕ + +Write scripts in **real Java** instead of JavaScript! Powered by the **Janino** compiler, your Java code is compiled and runs natively. + +- **Full Java Language** - Use classes, interfaces, enums, generics, lambdas, and everything you know from Java +- **Automatic Hook Resolution** - Just name your methods after the hook you want (e.g., `init`, `tick`, `interact`) and they're wired up automatically +- **Language Selector** - Each script tab has a dropdown to choose between JavaScript and Java. Mix and match across tabs +- **External Files** - Write `.java` scripts in an external editor and they're loaded automatically + +> Java and JavaScript scripts coexist side by side. Pick whichever language fits your workflow! + +--- + +## Client-Side Scripting 🖥️ + +Scripts can now run **on the client** for responsive, visual experiences! + +- **Server Controlled** - Server owners decide whether client scripting is allowed. Players can't enable it on their own +- **Automatic Sync** - Script files are sent from the server to all connected clients on login, and re-synced whenever scripts are reloaded +- **Safe by Default** - Client scripting is off by default and must be explicitly enabled in the server config + +> Client-side scripts open the door to custom UI effects, visual feedback, and client-only hooks -- all while the server stays in full control. + +--- + +## Cloner Tab Overhaul 📂 + +The Cloner now supports **folders** for organizing your saved NPCs, items, and entries! + +- **Folder System** - Create folders and subfolders to keep your cloner organized however you like +- **Full Screen Browser** - A new full-width directory view makes browsing large collections much easier +- **Improved Tabs** - Better tab navigation for switching between cloner categories + +--- + +## Addon API Support 📘 + +Addon developers can now ship **type definitions** alongside their mods, giving scripters full autocomplete and documentation for addon APIs! + +- **Automatic Loading** - Any mod can include API definitions and they'll appear in the script editor automatically +- **Patch Support** - Addons can extend existing CNPC+ types with new methods (e.g., a DBC addon adding `getDBCPlayer()` to the Player type) +- **Full Documentation** - Parameter names, return types, and descriptions all show up in autocomplete and hover info + +> If you're a scripter using addons, you get autocomplete for their APIs with zero setup. If you're an addon developer, just include your definitions and everything works. + +--- + +## Animation Improvements 🎬 + +- **Data Store** - Animations can now store and pass data across events and frames for more complex animation logic +- **Task System** - A new consumer-based system for reacting to animation lifecycle events, replacing the old approach +- **Expanded API** - More animation control exposed to the scripting API + +--- + +## Additional Changes 🔧 + +- **Player Dialog Events** - Now work the same way as NPC Dialog Events for consistency +- **Linked Item Attack Speed** - Added attack speed configuration to linked items +- **Trader Balance Preview** - Client-side balance prediction shows your expected balance during trades +- **Right-Click Cycling** - Multi-option NPC buttons (Hair, Eyes, Fur, etc.) can now be right-clicked to cycle backwards +- **Quest Cooldown Fix** - Fixed cooldown not working for MC Custom and RL Custom timer types +- **Bard Music Fix** - Fixed music restarting or duplicating when opening dialogs +- **Biome Spawn Fix** - Fixed NPC biome spawn settings not saving after editing +- **Block Waypoint Fix** - Fixed issues with block waypoints +- **Script Config Fix** - Fixed script configuration not syncing properly between server and client + +--- From bc9fd53ae8d33c5aedde4e2b7676f59a51c93b97 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 20:35:03 -0500 Subject: [PATCH 329/337] Beta 2 --- build.gradle | 2 +- src/main/java/noppes/npcs/CustomNpcs.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index a05d5d7a5..46c55e203 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id "dts.typescript-generator" } -version = "1.11-beta1" +version = "1.11-beta2" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 933450557..364598166 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -118,7 +118,7 @@ import java.util.Set; import java.util.function.Consumer; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta1") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta2") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") From 185d91592857814765c317d2e9b1512bc38d886c Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 13 Feb 2026 14:17:17 -0500 Subject: [PATCH 330/337] refactor: simplify NBT reading for ability properties and potion effects --- .../controllers/data/ability/Ability.java | 60 ++++------- .../data/ability/AbilityEffect.java | 99 +++++++------------ .../data/ability/data/EnergyAnchorData.java | 8 +- .../data/ability/data/EnergyCombatData.java | 12 +-- .../data/ability/data/EnergyDisplayData.java | 12 +-- .../data/ability/data/EnergyHomingData.java | 8 +- .../data/ability/data/EnergyLifespanData.java | 4 +- .../ability/data/EnergyLightningData.java | 8 +- .../ability/data/EnergyTrajectoryData.java | 6 +- .../data/ability/data/ProjectileData.java | 4 +- .../data/ability/gui/AbilityFieldBuilder.java | 53 +++++++--- .../data/ability/type/AbilityBeam.java | 8 +- .../data/ability/type/AbilityCharge.java | 14 +-- .../data/ability/type/AbilityCutter.java | 16 +-- .../data/ability/type/AbilityDash.java | 4 +- .../data/ability/type/AbilityDisc.java | 14 +-- .../data/ability/type/AbilityGuard.java | 24 ++--- .../data/ability/type/AbilityHazard.java | 10 +- .../data/ability/type/AbilityHeal.java | 14 +-- .../data/ability/type/AbilityHeavyHit.java | 8 +- .../data/ability/type/AbilityLaserShot.java | 6 +- .../data/ability/type/AbilityOrb.java | 6 +- .../data/ability/type/AbilityProjectile.java | 16 +-- .../data/ability/type/AbilityShockwave.java | 8 +- .../data/ability/type/AbilitySlam.java | 10 +- .../data/ability/type/AbilitySweeper.java | 18 ++-- .../data/ability/type/AbilityTeleport.java | 28 +++--- .../data/ability/type/AbilityTrap.java | 16 +-- .../data/ability/type/AbilityVortex.java | 16 +-- .../data/ability/type/AbilityZone.java | 63 ++++++------ src/main/java/noppes/npcs/DataStats.java | 16 ++- .../client/gui/SubGuiNpcMeleeProperties.java | 27 +++-- .../npcs/client/gui/SubGuiNpcProjectiles.java | 24 +++-- .../npcs/client/gui/util/GuiNPCInterface.java | 7 +- .../noppes/npcs/constants/EnumPotionType.java | 80 ++++++++++++++- .../npcs/entity/EntityNPCInterface.java | 35 ++----- .../noppes/npcs/entity/EntityProjectile.java | 82 ++++++--------- .../assets/customnpcs/lang/en_US.lang | 2 + 38 files changed, 429 insertions(+), 417 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index b2743f629..ed3638d40 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -1175,61 +1175,37 @@ public void readNBT(NBTTagCompound nbt) { maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); - syncWindupWithAnimation = !nbt.hasKey("syncWindup") || nbt.getBoolean("syncWindup"); + syncWindupWithAnimation = nbt.getBoolean("syncWindup"); dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); - if (nbt.hasKey("rotationMode")) { - // Current format - rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); - rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase")); - } else if (nbt.hasKey("lockType")) { - // Legacy format migration: lockType was LockType enum (0=MOVEMENT, 1=ROTATION, 2=BOTH) - int oldLockType = nbt.getInteger("lockType"); - if (oldLockType == 0) { - // MOVEMENT only: movement locked, rotation was free - rotationMode = RotationMode.FREE; - } else if (oldLockType == 1) { - // ROTATION only: rotation locked same timing as old lockMovement, movement was free - rotationMode = RotationMode.LOCKED; - rotationPhase = lockMovement; - lockMovement = LockMovementType.NO; - } else { - // BOTH: both locked same timing - rotationMode = RotationMode.LOCKED; - rotationPhase = lockMovement; - } - } else { - rotationMode = RotationMode.FREE; - rotationPhase = LockMovementType.WINDUP_AND_ACTIVE; - } + rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); + rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase")); windUpColor = nbt.getInteger("windUpColor"); activeColor = nbt.getInteger("activeColor"); windUpSound = nbt.getString("windUpSound"); activeSound = nbt.getString("activeSound"); - windUpAnimationId = nbt.hasKey("windUpAnimationId") ? nbt.getInteger("windUpAnimationId") : -1; - activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1; + windUpAnimationId = nbt.getInteger("windUpAnimationId"); + activeAnimationId = nbt.getInteger("activeAnimationId"); windUpAnimationName = nbt.getString("windUpAnimationName"); activeAnimationName = nbt.getString("activeAnimationName"); - showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph"); - if (nbt.hasKey("telegraphType")) { - try { - telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType")); - } catch (Exception e) { - telegraphType = TelegraphType.CIRCLE; - } + showTelegraph = nbt.getBoolean("showTelegraph"); + try { + telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType")); + } catch (Exception e) { + telegraphType = TelegraphType.CIRCLE; } - telegraphHeightOffset = nbt.hasKey("telegraphHeightOffset") ? nbt.getFloat("telegraphHeightOffset") : 0.1f; + telegraphHeightOffset = nbt.getFloat("telegraphHeightOffset"); customData = nbt.getCompoundTag("customData"); - allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH; - ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown"); + allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy")); + ignoreCooldown = nbt.getBoolean("ignoreCooldown"); // Burst - burstEnabled = nbt.hasKey("burstEnabled") && nbt.getBoolean("burstEnabled"); - burstAmount = nbt.hasKey("burstAmount") ? nbt.getInteger("burstAmount") : 0; - burstDelay = nbt.hasKey("burstDelay") ? nbt.getInteger("burstDelay") : 0; - burstReplayAnimations = !nbt.hasKey("burstReplayAnimations") || nbt.getBoolean("burstReplayAnimations"); - burstOverlap = nbt.hasKey("burstOverlap") && nbt.getBoolean("burstOverlap"); + burstEnabled = nbt.getBoolean("burstEnabled"); + burstAmount = nbt.getInteger("burstAmount"); + burstDelay = nbt.getInteger("burstDelay"); + burstReplayAnimations = nbt.getBoolean("burstReplayAnimations"); + burstOverlap = nbt.getBoolean("burstOverlap"); // Conditions conditions.clear(); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java index 73f465049..e2c1da87d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java @@ -2,67 +2,24 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; import net.minecraft.potion.PotionEffect; +import noppes.npcs.constants.EnumPotionType; /** * Represents a configurable potion effect that can be applied by abilities. - * Provides a unified system for managing debuffs like slowness, weakness, poison, etc. + * Uses the unified EnumPotionType enum shared across melee, ranged, and ability systems. */ public class AbilityEffect { - public enum EffectType { - NONE("gui.none", -1), - SLOWNESS("potion.moveSlowdown", Potion.moveSlowdown.id), - WEAKNESS("potion.weakness", Potion.weakness.id), - POISON("potion.poison", Potion.poison.id), - WITHER("potion.wither", Potion.wither.id), - BLINDNESS("potion.blindness", Potion.blindness.id), - NAUSEA("potion.confusion", Potion.confusion.id), - HUNGER("potion.hunger", Potion.hunger.id), - MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id), - BURN("ability.effect.burn", -1); - - private final String langKey; - private final int potionId; - - EffectType(String langKey, int potionId) { - this.langKey = langKey; - this.potionId = potionId; - } - - public String getLangKey() { - return langKey; - } - - public int getPotionId() { - return potionId; - } - - public static String[] getLangKeys() { - EffectType[] types = values(); - String[] keys = new String[types.length]; - for (int i = 0; i < types.length; i++) keys[i] = types[i].langKey; - return keys; - } - - public static EffectType fromOrdinal(int ordinal) { - EffectType[] values = values(); - if (ordinal >= 0 && ordinal < values.length) { - return values[ordinal]; - } - return NONE; - } - } - - private EffectType type = EffectType.NONE; + private EnumPotionType type = EnumPotionType.None; + private int manualPotionId = 0; private int durationTicks = 60; private int amplifier = 0; // 0-10 public AbilityEffect() { } - public AbilityEffect(EffectType type, int durationTicks, int amplifier) { + public AbilityEffect(EnumPotionType type, int durationTicks, int amplifier) { this.type = type; this.durationTicks = Math.max(1, durationTicks); this.amplifier = Math.max(0, Math.min(10, amplifier)); @@ -72,25 +29,27 @@ public AbilityEffect(EffectType type, int durationTicks, int amplifier) { * Creates a copy of this effect. */ public AbilityEffect copy() { - return new AbilityEffect(type, durationTicks, amplifier); + AbilityEffect copy = new AbilityEffect(type, durationTicks, amplifier); + copy.manualPotionId = this.manualPotionId; + return copy; } /** * Applies this effect to the given entity. - * Does nothing if type is NONE. + * Does nothing if type is None. Validates potion IDs before applying. */ public void apply(EntityLivingBase entity) { - if (entity == null || type == EffectType.NONE) { + if (entity == null || type == EnumPotionType.None) { return; } - if (type == EffectType.BURN) { + if (type == EnumPotionType.Fire) { entity.setFire(Math.max(1, durationTicks / 20)); return; } - int potionId = type.getPotionId(); - if (potionId >= 0) { + int potionId = type.getResolvedPotionId(manualPotionId); + if (EnumPotionType.isValidPotionId(potionId)) { entity.addPotionEffect(new PotionEffect(potionId, durationTicks, amplifier)); } } @@ -100,9 +59,12 @@ public void apply(EntityLivingBase entity) { */ public NBTTagCompound writeNBT() { NBTTagCompound nbt = new NBTTagCompound(); - nbt.setInteger("type", type.ordinal()); + nbt.setInteger("potionType", type.ordinal()); nbt.setInteger("duration", durationTicks); nbt.setInteger("amplifier", amplifier); + if (type == EnumPotionType.Manual) { + nbt.setInteger("manualPotionId", manualPotionId); + } return nbt; } @@ -110,9 +72,12 @@ public NBTTagCompound writeNBT() { * Reads this effect from NBT. */ public void readNBT(NBTTagCompound nbt) { - this.type = EffectType.fromOrdinal(nbt.getInteger("type")); - this.durationTicks = nbt.hasKey("duration") ? nbt.getInteger("duration") : 60; - this.amplifier = nbt.hasKey("amplifier") ? nbt.getInteger("amplifier") : 0; + this.type = EnumPotionType.fromOrdinal(nbt.getInteger("potionType")); + this.durationTicks = nbt.getInteger("duration"); + this.amplifier = nbt.getInteger("amplifier"); + if (type == EnumPotionType.Manual) { + this.manualPotionId = nbt.getInteger("manualPotionId"); + } } /** @@ -126,12 +91,20 @@ public static AbilityEffect fromNBT(NBTTagCompound nbt) { // Getters & Setters - public EffectType getType() { + public EnumPotionType getType() { return type; } - public void setType(EffectType type) { - this.type = type != null ? type : EffectType.NONE; + public void setType(EnumPotionType type) { + this.type = type != null ? type : EnumPotionType.None; + } + + public int getManualPotionId() { + return manualPotionId; + } + + public void setManualPotionId(int manualPotionId) { + this.manualPotionId = Math.max(0, manualPotionId); } public int getDurationTicks() { @@ -151,9 +124,9 @@ public void setAmplifier(int amplifier) { } /** - * Returns true if this effect is configured (not NONE). + * Returns true if this effect is configured (not None). */ public boolean isValid() { - return type != EffectType.NONE; + return type != EnumPotionType.None; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java index 2187802a5..99c51b23c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java @@ -91,10 +91,10 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromOrdinal(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; - this.anchorOffsetX = nbt.hasKey("anchorOffsetX") ? nbt.getFloat("anchorOffsetX") : 0; - this.anchorOffsetY = nbt.hasKey("anchorOffsetY") ? nbt.getFloat("anchorOffsetY") : 0; - this.anchorOffsetZ = nbt.hasKey("anchorOffsetZ") ? nbt.getFloat("anchorOffsetZ") : 0; + this.anchorPoint = AnchorPoint.fromOrdinal(nbt.getInteger("anchorPoint")); + this.anchorOffsetX = nbt.getFloat("anchorOffsetX"); + this.anchorOffsetY = nbt.getFloat("anchorOffsetY"); + this.anchorOffsetZ = nbt.getFloat("anchorOffsetZ"); } public EnergyAnchorData copy() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java index 52f5368f0..e6ccf15a1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java @@ -97,12 +97,12 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; - knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; - knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; - explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; - explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + damage = nbt.getFloat("damage"); + knockback = nbt.getFloat("knockback"); + knockbackUp = nbt.getFloat("knockbackUp"); + explosive = nbt.getBoolean("explosive"); + explosionRadius = nbt.getFloat("explosionRadius"); + explosionDamageFalloff = nbt.getFloat("explosionDamageFalloff"); } public EnergyCombatData copy() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java index 0d91e1012..559be77f4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyDisplayData.java @@ -102,12 +102,12 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; - outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; - outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); - outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; - outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; - rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; + innerColor = nbt.getInteger("innerColor"); + outerColor = nbt.getInteger("outerColor"); + outerColorEnabled = nbt.getBoolean("outerColorEnabled"); + outerColorWidth = nbt.getFloat("outerColorWidth"); + outerColorAlpha = nbt.getFloat("outerColorAlpha"); + rotationSpeed = nbt.getFloat("rotationSpeed"); } public EnergyDisplayData copy() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java index 1e81882c7..d385c06e7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java @@ -71,10 +71,10 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.5f; - homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); - homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; - homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; + speed = nbt.getFloat("speed"); + homing = nbt.getBoolean("homing"); + homingStrength = nbt.getFloat("homingStrength"); + homingRange = nbt.getFloat("homingRange"); } public EnergyHomingData copy() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java index 9e017e2a5..f320acdb0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java @@ -44,8 +44,8 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; - maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; + maxDistance = nbt.getFloat("maxDistance"); + maxLifetime = nbt.getInteger("maxLifetime"); } public EnergyLifespanData copy() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java index 00d0eaa96..66786a14a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java @@ -71,10 +71,10 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); - lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; - lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; - lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; + lightningEffect = nbt.getBoolean("lightningEffect"); + lightningDensity = nbt.getFloat("lightningDensity"); + lightningRadius = nbt.getFloat("lightningRadius"); + lightningFadeTime = nbt.getInteger("lightningFadeTime"); } public EnergyLightningData copy() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java index 7cc15a706..15adb3062 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyTrajectoryData.java @@ -184,7 +184,7 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - this.currentPath = nbt.hasKey("currentPath") ? nbt.getInteger("currentPath") : 0; + this.currentPath = nbt.getInteger("currentPath"); int i = 0; while (nbt.hasKey("Path_" + i, Constants.NBT.TAG_COMPOUND)) { @@ -275,8 +275,8 @@ public void writeNBT(NBTTagCompound nbt) { } public void readNBT(NBTTagCompound nbt) { - this.delayTicks = nbt.hasKey("delay") ? nbt.getInteger("delay") : 0; - this.concluded = nbt.hasKey("concluded") && nbt.getBoolean("concluded"); + this.delayTicks = nbt.getInteger("delay"); + this.concluded = nbt.getBoolean("concluded"); double x = nbt.getDouble("X"); double y = nbt.getDouble("Y"); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java index 49ee50f7e..94850470f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java @@ -44,8 +44,8 @@ public void readNBT(NBTTagCompound nbt) { anchor.readNBT(nbt); colorOverride = nbt.getBoolean("colorOverride"); if (colorOverride) { - innerColor = nbt.hasKey("overrideInnerColor") ? nbt.getInteger("overrideInnerColor") : 0xFFFFFF; - outerColor = nbt.hasKey("overrideOuterColor") ? nbt.getInteger("overrideOuterColor") : 0x8888FF; + innerColor = nbt.getInteger("overrideInnerColor"); + outerColor = nbt.getInteger("overrideOuterColor"); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java index 5edd0a718..219479614 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldBuilder.java @@ -12,6 +12,7 @@ import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.constants.EnumPotionType; import java.util.ArrayList; import java.util.HashMap; @@ -26,7 +27,7 @@ public class AbilityFieldBuilder extends GuiFieldBuilder { // Effects list metadata: widgetId -> [effectIndex, action] - // action: 0=type, 1=duration(text), 2=amp, 3=delete, 4=add + // action: 0=type, 1=duration(text), 2=amp, 3=delete, 4=add, 5=manualId(text) private final Map effectWidgetMeta = new HashMap<>(); public AbilityFieldBuilder(GuiNPCInterface parent, FontRenderer fontRenderer) { @@ -57,15 +58,15 @@ private int renderEffectsList(FieldDef def, int y) { List effects = (List) def.getValue(); if (effects == null) effects = new ArrayList<>(); - String[] typeNames = AbilityEffect.EffectType.getLangKeys(); + String[] typeNames = EnumPotionType.getLangKeysNoNone(); String[] ampValues = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; for (int e = 0; e < effects.size() && e < 5; e++) { AbilityEffect effect = effects.get(e); - // Type selector button - int typeIdx = effect.getType().ordinal(); - GuiNpcButton typeBtn = new GuiNpcButton(widgetId, colLLabel, y, 100, 20, typeNames, typeIdx); + // Type selector button (index offset by -1 since None is excluded) + int typeIdx = effect.getType().ordinal() - 1; + GuiNpcButton typeBtn = new GuiNpcButton(widgetId, colLLabel, y, 100, 20, typeNames, Math.max(0, typeIdx)); sw.addButton(typeBtn); buttonFieldMap.put(widgetId, def); effectWidgetMeta.put(widgetId, new int[]{e, 0}); @@ -81,12 +82,16 @@ private int renderEffectsList(FieldDef def, int y) { effectWidgetMeta.put(widgetId, new int[]{e, 1}); widgetId++; - // Amplifier selector (0-10) - GuiNpcButton ampBtn = new GuiNpcButton(widgetId, colLLabel + 158, y, 40, 20, ampValues, effect.getAmplifier()); - sw.addButton(ampBtn); - buttonFieldMap.put(widgetId, def); - effectWidgetMeta.put(widgetId, new int[]{e, 2}); - widgetId++; + if (effect.getType() != EnumPotionType.Fire) { + // Amplifier selector (0-10) — not applicable to Fire + GuiNpcButton ampBtn = new GuiNpcButton(widgetId, colLLabel + 158, y, 40, 20, ampValues, effect.getAmplifier()); + sw.addButton(ampBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 2}); + widgetId++; + } else { + widgetId++; // keep widgetId in sync + } // Delete button GuiNpcButton delBtn = new GuiNpcButton(clearId, colLLabel + 202, y, 20, 20, "X"); @@ -96,6 +101,20 @@ private int renderEffectsList(FieldDef def, int y) { clearId++; y += rowHeight; + + // Manual potion ID field (shown on next row when type is Manual) + if (effect.getType() == EnumPotionType.Manual) { + sw.addLabel(new GuiNpcLabel(labelId++, "effect.potionid", colLLabel, y + 5, 0xFFFFFF)); + GuiNpcTextField idField = new GuiNpcTextField(widgetId, parent, fontRenderer, + colLLabel + 80, y, 50, 20, String.valueOf(effect.getManualPotionId())); + idField.setIntegersOnly(); + idField.setMinMaxDefault(0, Integer.MAX_VALUE, 0); + sw.addTextField(idField); + textFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 5}); + widgetId++; + y += rowHeight; + } } // Add button (if < 5 effects) @@ -133,7 +152,7 @@ public boolean handleButtonEvent(int buttonId, GuiButton button) { switch (action) { case 0: // Type changed if (effectIdx < effects.size()) { - effects.get(effectIdx).setType(AbilityEffect.EffectType.fromOrdinal(((GuiNpcButton) button).getValue())); + effects.get(effectIdx).setType(EnumPotionType.fromIndexNoNone(((GuiNpcButton) button).getValue())); } return true; case 2: // Amp changed @@ -148,7 +167,7 @@ public boolean handleButtonEvent(int buttonId, GuiButton button) { return true; case 4: // Add if (effects.size() < 5) { - effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 60, 0)); + effects.add(new AbilityEffect(EnumPotionType.Slowness, 60, 0)); } return true; } @@ -162,12 +181,16 @@ public boolean handleButtonEvent(int buttonId, GuiButton button) { @SuppressWarnings("unchecked") public boolean handleTextFieldEvent(int textFieldId, GuiNpcTextField field) { int[] meta = effectWidgetMeta.get(textFieldId); - if (meta != null && meta[1] == 1) { // duration field + if (meta != null) { FieldDef def = textFieldMap.get(textFieldId); if (def != null && def.getType() == FieldType.EFFECTS_LIST) { List effects = (List) def.getValue(); if (effects != null && meta[0] < effects.size()) { - effects.get(meta[0]).setDurationTicks(field.getInteger()); + if (meta[1] == 1) { // duration field + effects.get(meta[0]).setDurationTicks(field.getInteger()); + } else if (meta[1] == 5) { // manual potion ID field + effects.get(meta[0]).setManualPotionId(field.getInteger()); + } } return true; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 8bf2341f5..d02c19bdd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -356,13 +356,13 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; - this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + this.beamWidth = nbt.getFloat("beamWidth"); + this.headSize = nbt.getFloat("headSize"); - int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; + int count = nbt.getInteger("projectileCount"); initProjectiles(count); - this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0; + this.fireDelay = nbt.getInteger("fireDelay"); combatData.readNBT(nbt); homingData.readNBT(nbt); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index a7430ce20..50257e023 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -334,16 +334,10 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.chargeSpeed = nbt.hasKey("chargeSpeed") ? nbt.getFloat("chargeSpeed") : 0.8f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 3.0f; - if (nbt.hasKey("hitWidth")) { - this.hitWidth = nbt.getFloat("hitWidth"); - } else if (nbt.hasKey("hitRadius")) { - this.hitWidth = nbt.getFloat("hitRadius"); - } else { - this.hitWidth = 1.5f; - } + this.chargeSpeed = nbt.getFloat("chargeSpeed"); + this.damage = nbt.getFloat("damage"); + this.knockback = nbt.getFloat("knockback"); + this.hitWidth = nbt.getFloat("hitWidth"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 900ae6080..110bda19f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -326,19 +326,19 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.arcAngle = nbt.hasKey("arcAngle") ? nbt.getFloat("arcAngle") : 90.0f; - this.range = nbt.hasKey("range") ? nbt.getFloat("range") : 6.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; + this.arcAngle = nbt.getFloat("arcAngle"); + this.range = nbt.getFloat("range"); + this.damage = nbt.getFloat("damage"); + this.knockback = nbt.getFloat("knockback"); try { this.sweepMode = SweepMode.valueOf(nbt.getString("sweepMode")); } catch (Exception e) { this.sweepMode = SweepMode.SWIPE; } - this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f; - this.spinDurationTicks = nbt.hasKey("spinDurationTicks") ? nbt.getInteger("spinDurationTicks") : 60; - this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); - this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; + this.sweepSpeed = nbt.getFloat("sweepSpeed"); + this.spinDurationTicks = nbt.getInteger("spinDurationTicks"); + this.piercing = nbt.getBoolean("piercing"); + this.innerRadius = nbt.getFloat("innerRadius"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 0f8cbe374..7e3375ca7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -284,8 +284,8 @@ public void readTypeNBT(NBTTagCompound nbt) { } catch (Exception e) { this.dashMode = DashMode.DEFENSIVE; } - this.dashDistance = nbt.hasKey("dashDistance") ? nbt.getFloat("dashDistance") : 4.0f; - this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 0.5f; + this.dashDistance = nbt.getFloat("dashDistance"); + this.dashSpeed = nbt.getFloat("dashSpeed"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 886a269e2..2448642e0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -368,16 +368,16 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; - this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; - this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical"); - this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); - this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + this.discRadius = nbt.getFloat("discRadius"); + this.discThickness = nbt.getFloat("discThickness"); + this.vertical = nbt.getBoolean("vertical"); + this.boomerang = nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.getInteger("boomerangDelay"); - int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; + int count = nbt.getInteger("projectileCount"); initProjectiles(count); - this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0; + this.fireDelay = nbt.getInteger("fireDelay"); // Shared combat/movement data combatData.readNBT(nbt); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 461fef2bd..87fd21a7f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -238,27 +238,19 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; - this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f; - this.canCounter = nbt.hasKey("canCounter") && nbt.getBoolean("canCounter"); - this.counterWindow = nbt.hasKey("counterWindow") ? nbt.getInteger("counterWindow") : 10; + this.durationTicks = nbt.getInteger("durationTicks"); + this.damageReduction = nbt.getFloat("damageReduction"); + this.canCounter = nbt.getBoolean("canCounter"); + this.counterWindow = nbt.getInteger("counterWindow"); try { this.counterType = CounterType.valueOf(nbt.getString("counterType")); } catch (Exception e) { this.counterType = CounterType.FLAT; } - if (nbt.hasKey("counterValue")) { - this.counterValue = nbt.getFloat("counterValue"); - } else if (nbt.hasKey("counterDamage")) { - this.counterValue = nbt.getFloat("counterDamage"); - } else { - this.counterValue = 6.0f; - } - this.counterSound = nbt.hasKey("counterSound") ? nbt.getString("counterSound") : "random.wood_click"; - this.counterAnimationId = nbt.hasKey("counterAnimationId") ? nbt.getInteger("counterAnimationId") : -1; - this.counterAnimationName = nbt.hasKey("counterAnimationName") ? - nbt.getString("counterAnimationName") : - "Ability_Guard_Counter"; + this.counterValue = nbt.getFloat("counterValue"); + this.counterSound = nbt.getString("counterSound"); + this.counterAnimationId = nbt.getInteger("counterAnimationId"); + this.counterAnimationName = nbt.getString("counterAnimationName"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index a8bbafc1a..e5dfde01e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -115,11 +115,11 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { readZoneNBT(nbt, 300); - this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 2.0f; - this.damagePerSecond = nbt.hasKey("damagePerSecond") ? nbt.getFloat("damagePerSecond") : 1.0f; - this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20; - this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames"); - this.affectsCaster = nbt.hasKey("affectsCaster") && nbt.getBoolean("affectsCaster"); + this.radius = nbt.getFloat("radius"); + this.damagePerSecond = nbt.getFloat("damagePerSecond"); + this.damageInterval = nbt.getInteger("damageInterval"); + this.ignoreInvulnFrames = nbt.getBoolean("ignoreInvulnFrames"); + this.affectsCaster = nbt.getBoolean("affectsCaster"); } // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index d913fe586..d2714ddd2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -227,13 +227,13 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; - this.healAmount = nbt.hasKey("healAmount") ? nbt.getFloat("healAmount") : 10.0f; - this.healPercent = nbt.hasKey("healPercent") ? nbt.getFloat("healPercent") : 0.0f; - this.healSelf = !nbt.hasKey("healSelf") || nbt.getBoolean("healSelf"); - this.healAllies = nbt.hasKey("healAllies") && nbt.getBoolean("healAllies"); - this.healRadius = nbt.hasKey("healRadius") ? nbt.getFloat("healRadius") : 0.0f; - this.instantHeal = !nbt.hasKey("instantHeal") || nbt.getBoolean("instantHeal"); + this.durationTicks = nbt.getInteger("durationTicks"); + this.healAmount = nbt.getFloat("healAmount"); + this.healPercent = nbt.getFloat("healPercent"); + this.healSelf = nbt.getBoolean("healSelf"); + this.healAllies = nbt.getBoolean("healAllies"); + this.healRadius = nbt.getFloat("healRadius"); + this.instantHeal = nbt.getBoolean("instantHeal"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index dd5d5fc09..cf49cade6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -135,10 +135,10 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 2.0f; - this.hitLength = nbt.hasKey("hitLength") ? nbt.getFloat("hitLength") : 4.0f; - this.hitWidth = nbt.hasKey("hitWidth") ? nbt.getFloat("hitWidth") : 3.0f; + this.damage = nbt.getFloat("damage"); + this.knockback = nbt.getFloat("knockback"); + this.hitLength = nbt.getFloat("hitLength"); + this.hitWidth = nbt.getFloat("hitWidth"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index afffa9c13..a726e4c25 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -190,9 +190,9 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.laserWidth = nbt.hasKey("laserWidth") ? nbt.getFloat("laserWidth") : 0.3f; - this.expansionSpeed = nbt.hasKey("expansionSpeed") ? nbt.getFloat("expansionSpeed") : 3.0f; - this.lingerTicks = nbt.hasKey("lingerTicks") ? nbt.getInteger("lingerTicks") : 8; + this.laserWidth = nbt.getFloat("laserWidth"); + this.expansionSpeed = nbt.getFloat("expansionSpeed"); + this.lingerTicks = nbt.getInteger("lingerTicks"); anchorData.readNBT(nbt); colorData.readNBT(nbt); combatData.readNBT(nbt); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 8b4783f7c..5dbf58424 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -364,12 +364,12 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; + this.orbSize = nbt.getFloat("orbSize"); - int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; + int count = nbt.getInteger("projectileCount"); initProjectiles(count); - this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0; + this.fireDelay = nbt.getInteger("fireDelay"); // Shared combat/movement data combatData.readNBT(nbt); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 8abd5d01e..d79f783d1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -273,14 +273,14 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; - this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 1.5f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; - this.projectileType = nbt.hasKey("projectileType") ? nbt.getString("projectileType") : "fireball"; - this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 0.0f; - this.homing = nbt.hasKey("homing") && nbt.getBoolean("homing"); - this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.1f; + this.damage = nbt.getFloat("damage"); + this.speed = nbt.getFloat("speed"); + this.knockback = nbt.getFloat("knockback"); + this.projectileType = nbt.getString("projectileType"); + this.explosive = nbt.getBoolean("explosive"); + this.explosionRadius = nbt.getFloat("explosionRadius"); + this.homing = nbt.getBoolean("homing"); + this.homingStrength = nbt.getFloat("homingStrength"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 0ef0dc50f..17fdecceb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -127,10 +127,10 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.pushRadius = nbt.hasKey("pushRadius") ? nbt.getFloat("pushRadius") : 8.0f; - this.pushStrength = nbt.hasKey("pushStrength") ? nbt.getFloat("pushStrength") : 1.5f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; - this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 10; + this.pushRadius = nbt.getFloat("pushRadius"); + this.pushStrength = nbt.getFloat("pushStrength"); + this.damage = nbt.getFloat("damage"); + this.maxTargets = nbt.getInteger("maxTargets"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index a03ecdf6a..0bd9613a5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -490,11 +490,11 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; - radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 5.0f; - knockbackStrength = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; - leapSpeed = nbt.hasKey("leapSpeed") ? nbt.getFloat("leapSpeed") : 1.0f; - leapHeight = nbt.hasKey("leapHeight") ? nbt.getFloat("leapHeight") : 4.0f; + damage = nbt.getFloat("damage"); + radius = nbt.getFloat("radius"); + knockbackStrength = nbt.getFloat("knockback"); + leapSpeed = nbt.getFloat("leapSpeed"); + leapHeight = nbt.getFloat("leapHeight"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 8638dd2a4..43a152f7a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -172,15 +172,15 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; - this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.3f; - this.beamHeight = nbt.hasKey("beamHeight") ? nbt.getFloat("beamHeight") : 0.5f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; - this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; - this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); - this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 3.0f; - this.numberOfRotations = nbt.hasKey("numberOfRotations") ? nbt.getInteger("numberOfRotations") : 2; - this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); + this.beamLength = nbt.getFloat("beamLength"); + this.beamWidth = nbt.getFloat("beamWidth"); + this.beamHeight = nbt.getFloat("beamHeight"); + this.damage = nbt.getFloat("damage"); + this.damageInterval = nbt.getInteger("damageInterval"); + this.piercing = nbt.getBoolean("piercing"); + this.sweepSpeed = nbt.getFloat("sweepSpeed"); + this.numberOfRotations = nbt.getInteger("numberOfRotations"); + this.lockOnTarget = nbt.getBoolean("lockOnTarget"); colorData.readNBT(nbt); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 29eb73832..d9e05c382 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -430,24 +430,20 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - if (nbt.hasKey("mode")) { - try { - this.mode = TeleportMode.valueOf(nbt.getString("mode")); - } catch (Exception e) { - this.mode = TeleportMode.BLINK; - } - } else { + try { + this.mode = TeleportMode.valueOf(nbt.getString("mode")); + } catch (Exception e) { this.mode = TeleportMode.BLINK; } - this.blinkCount = nbt.hasKey("blinkCount") ? nbt.getInteger("blinkCount") : 3; - this.blinkDelayTicks = nbt.hasKey("blinkDelayTicks") ? nbt.getInteger("blinkDelayTicks") : 10; - this.blinkRadius = nbt.hasKey("blinkRadius") ? nbt.getFloat("blinkRadius") : 8.0f; - this.behindDistance = nbt.hasKey("behindDistance") ? nbt.getFloat("behindDistance") : 2.0f; - this.requireLineOfSight = !nbt.hasKey("requireLineOfSight") || nbt.getBoolean("requireLineOfSight"); - this.damageAtStart = nbt.hasKey("damageAtStart") && nbt.getBoolean("damageAtStart"); - this.damageAtEnd = nbt.hasKey("damageAtEnd") && nbt.getBoolean("damageAtEnd"); - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; - this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 2.0f; + this.blinkCount = nbt.getInteger("blinkCount"); + this.blinkDelayTicks = nbt.getInteger("blinkDelayTicks"); + this.blinkRadius = nbt.getFloat("blinkRadius"); + this.behindDistance = nbt.getFloat("behindDistance"); + this.requireLineOfSight = nbt.getBoolean("requireLineOfSight"); + this.damageAtStart = nbt.getBoolean("damageAtStart"); + this.damageAtEnd = nbt.getBoolean("damageAtEnd"); + this.damage = nbt.getFloat("damage"); + this.damageRadius = nbt.getFloat("damageRadius"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index b35891d9a..84bafc726 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -119,14 +119,14 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { readZoneNBT(nbt, 300); - this.triggerRadius = nbt.hasKey("triggerRadius") ? nbt.getFloat("triggerRadius") : 2.0f; - this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20; - this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1; - this.triggerCooldown = nbt.hasKey("triggerCooldown") ? nbt.getInteger("triggerCooldown") : 20; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; - this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; - this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible"); + this.triggerRadius = nbt.getFloat("triggerRadius"); + this.armTime = nbt.getInteger("armTime"); + this.maxTriggers = nbt.getInteger("maxTriggers"); + this.triggerCooldown = nbt.getInteger("triggerCooldown"); + this.damage = nbt.getFloat("damage"); + this.damageRadius = nbt.getFloat("damageRadius"); + this.knockback = nbt.getFloat("knockback"); + this.visible = nbt.getBoolean("visible"); } // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 90aaf054b..d9d253d29 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -236,14 +236,14 @@ public void writeTypeNBT(NBTTagCompound nbt) { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.pullRadius = nbt.hasKey("pullRadius") ? nbt.getFloat("pullRadius") : 8.0f; - this.pullStrength = nbt.hasKey("pullStrength") ? nbt.getFloat("pullStrength") : 0.8f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 0.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.0f; - this.aoe = nbt.hasKey("aoe") && nbt.getBoolean("aoe"); - this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 5; - this.damageOnPull = nbt.hasKey("damageOnPull") && nbt.getBoolean("damageOnPull"); - this.pullDamage = nbt.hasKey("pullDamage") ? nbt.getFloat("pullDamage") : 0.0f; + this.pullRadius = nbt.getFloat("pullRadius"); + this.pullStrength = nbt.getFloat("pullStrength"); + this.damage = nbt.getFloat("damage"); + this.knockback = nbt.getFloat("knockback"); + this.aoe = nbt.getBoolean("aoe"); + this.maxTargets = nbt.getInteger("maxTargets"); + this.damageOnPull = nbt.getBoolean("damageOnPull"); + this.pullDamage = nbt.getFloat("pullDamage"); } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 8a15c2ce5..9a296e836 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -15,6 +15,7 @@ import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumPotionType; import net.minecraft.util.DamageSource; import net.minecraft.world.World; @@ -255,7 +256,7 @@ private void applyPresetDefaults(String styleName) { particleMotion = 1; particleGlow = true; particleDir = "mc:mobSpell"; colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611; windUpColor = 0x6044DD44; activeColor = 0xC044FF44; - effects.add(new AbilityEffect(AbilityEffect.EffectType.POISON, 100, 0)); + effects.add(new AbilityEffect(EnumPotionType.Poison, 100, 0)); break; case "INFERNO": groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3; @@ -264,7 +265,7 @@ private void applyPresetDefaults(String styleName) { particleMotion = 0; particleGlow = true; particleDir = "mc:flame"; colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200; windUpColor = 0x60FF6611; activeColor = 0xC0FF4400; - effects.add(new AbilityEffect(AbilityEffect.EffectType.BURN, 60, 0)); + effects.add(new AbilityEffect(EnumPotionType.Fire, 60, 0)); break; case "ARCANE": groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1; @@ -273,7 +274,7 @@ private void applyPresetDefaults(String styleName) { particleMotion = 1; particleGlow = true; particleDir = "mc:portal"; colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB; windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF; - effects.add(new AbilityEffect(AbilityEffect.EffectType.WEAKNESS, 100, 0)); + effects.add(new AbilityEffect(EnumPotionType.Weakness, 100, 0)); break; case "ELECTRIC": groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1; @@ -282,7 +283,7 @@ private void applyPresetDefaults(String styleName) { particleMotion = 2; particleGlow = true; particleDir = "mc:enchantmenttable"; colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB; windUpColor = 0x604488FF; activeColor = 0xC066AAFF; - effects.add(new AbilityEffect(AbilityEffect.EffectType.MINING_FATIGUE, 80, 1)); + effects.add(new AbilityEffect(EnumPotionType.MiningFatigue, 80, 1)); break; case "FROST": groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; @@ -291,7 +292,7 @@ private void applyPresetDefaults(String styleName) { particleMotion = 1; particleGlow = true; particleDir = "mc:snowshovel"; colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC; windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF; - effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 100, 1)); + effects.add(new AbilityEffect(EnumPotionType.Slowness, 100, 1)); break; case "DEFAULT": groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; @@ -354,37 +355,33 @@ protected void writeZoneNBT(NBTTagCompound nbt) { } protected void readZoneNBT(NBTTagCompound nbt, int defaultDuration) { - this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : defaultDuration; - if (nbt.hasKey("zoneShape")) { - try { this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape")); } - catch (Exception e) { this.zoneShape = ZoneShape.CIRCLE; } - } else { - this.zoneShape = ZoneShape.CIRCLE; - } - this.spawnRadius = nbt.hasKey("spawnRadius") ? nbt.getFloat("spawnRadius") : 10.0f; - this.zoneCount = nbt.hasKey("zoneCount") ? nbt.getInteger("zoneCount") : 3; - this.zoneHeight = nbt.hasKey("zoneHeight") ? nbt.getFloat("zoneHeight") : 2.0f; - this.particleDensity = nbt.hasKey("particleDensity") ? nbt.getFloat("particleDensity") : 1.0f; - this.particleScale = nbt.hasKey("particleScale") ? nbt.getFloat("particleScale") : 1.0f; - this.animSpeed = nbt.hasKey("animSpeed") ? nbt.getFloat("animSpeed") : 1.0f; - this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 1.0f; + this.durationTicks = nbt.getInteger("durationTicks"); + try { this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape")); } + catch (Exception e) { this.zoneShape = ZoneShape.CIRCLE; } + this.spawnRadius = nbt.getFloat("spawnRadius"); + this.zoneCount = nbt.getInteger("zoneCount"); + this.zoneHeight = nbt.getFloat("zoneHeight"); + this.particleDensity = nbt.getFloat("particleDensity"); + this.particleScale = nbt.getFloat("particleScale"); + this.animSpeed = nbt.getFloat("animSpeed"); + this.lightningDensity = nbt.getFloat("lightningDensity"); colorData.readNBT(nbt); // Visual layer fields - this.groundFill = !nbt.hasKey("groundFill") || nbt.getBoolean("groundFill"); - this.groundAlpha = nbt.hasKey("groundAlpha") ? nbt.getFloat("groundAlpha") : 0.25f; - this.rings = !nbt.hasKey("rings") || nbt.getBoolean("rings"); - this.ringCount = nbt.hasKey("ringCount") ? nbt.getInteger("ringCount") : 3; - this.border = !nbt.hasKey("border") || nbt.getBoolean("border"); - this.borderSpeed = nbt.hasKey("borderSpeed") ? nbt.getFloat("borderSpeed") : 1.0f; - this.accents = !nbt.hasKey("accents") || nbt.getBoolean("accents"); - this.accentStyle = nbt.hasKey("accentStyle") ? nbt.getInteger("accentStyle") : 0; - this.lightning = nbt.hasKey("lightning") && nbt.getBoolean("lightning"); - this.particles = !nbt.hasKey("particles") || nbt.getBoolean("particles"); - this.particleMotion = nbt.hasKey("particleMotion") ? nbt.getInteger("particleMotion") : 0; - this.particleDir = nbt.hasKey("particleDir") ? nbt.getString("particleDir") : ""; - this.particleSize = nbt.hasKey("particleSize") ? nbt.getInteger("particleSize") : 32; - this.particleGlow = !nbt.hasKey("particleGlow") || nbt.getBoolean("particleGlow"); + this.groundFill = nbt.getBoolean("groundFill"); + this.groundAlpha = nbt.getFloat("groundAlpha"); + this.rings = nbt.getBoolean("rings"); + this.ringCount = nbt.getInteger("ringCount"); + this.border = nbt.getBoolean("border"); + this.borderSpeed = nbt.getFloat("borderSpeed"); + this.accents = nbt.getBoolean("accents"); + this.accentStyle = nbt.getInteger("accentStyle"); + this.lightning = nbt.getBoolean("lightning"); + this.particles = nbt.getBoolean("particles"); + this.particleMotion = nbt.getInteger("particleMotion"); + this.particleDir = nbt.getString("particleDir"); + this.particleSize = nbt.getInteger("particleSize"); + this.particleGlow = nbt.getBoolean("particleGlow"); } // ═════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/DataStats.java b/src/main/java/noppes/npcs/DataStats.java index 0d085d823..7db32bc66 100644 --- a/src/main/java/noppes/npcs/DataStats.java +++ b/src/main/java/noppes/npcs/DataStats.java @@ -20,6 +20,7 @@ public class DataStats { public EnumPotionType potionType = EnumPotionType.None; public int potionDuration = 5; //20 = 1 second public int potionAmp = 0; + public int potionManualId = 0; public double maxHealth = 20; public int respawnTime = 20; @@ -45,6 +46,7 @@ public class DataStats { public boolean pPhysics = true, pXlr8 = false, pGlows = false, pExplode = false; public boolean pRender3D = false, pSpin = false, pStick = false, pBurnItem = false; public EnumPotionType pEffect = EnumPotionType.None; + public int pManualId = 0; public EnumParticleType pTrail = EnumParticleType.None; public ScriptParticle pCustom = new ScriptParticle(""); public int pEffAmp = 0; @@ -88,6 +90,8 @@ public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setInteger("PotionEffect", potionType.ordinal()); compound.setInteger("PotionDuration", potionDuration); compound.setInteger("PotionAmp", potionAmp); + if (potionType == EnumPotionType.Manual) + compound.setInteger("PotionManualId", potionManualId); compound.setInteger("MaxFiringRange", rangedRange); compound.setInteger("FireRate", fireRate); @@ -122,6 +126,8 @@ public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setTag("pCustom", pCustom.writeToNBT()); if (pEffect == EnumPotionType.Fire) compound.setBoolean("pBurnItem", pBurnItem); + if (pEffect == EnumPotionType.Manual) + compound.setInteger("pManualId", pManualId); compound.setBoolean("ImmuneToFire", immuneToFire); compound.setBoolean("PotionImmune", potionImmune); @@ -145,7 +151,7 @@ public void readToNBT(NBTTagCompound compound) { aggroRange = compound.getInteger("AggroRange"); respawnTime = compound.getInteger("RespawnTime"); spawnCycle = compound.getInteger("SpawnCycle"); - creatureType = EnumCreatureAttribute.values()[compound.getInteger("CreatureType") % EnumPotionType.values().length]; + creatureType = EnumCreatureAttribute.values()[compound.getInteger("CreatureType") % EnumCreatureAttribute.values().length]; ignoreCobweb = compound.getBoolean("IgnoreCobweb"); healthRegen = compound.getFloat("HealthRegen"); combatRegen = compound.getFloat("CombatRegen"); @@ -155,9 +161,11 @@ public void readToNBT(NBTTagCompound compound) { swingWarmUp = ValueUtil.clamp(compound.getInteger("SwingWarmup"), 0, 1000); attackRange = compound.getInteger("AttackRange"); knockback = compound.getInteger("KnockBack"); - potionType = EnumPotionType.values()[compound.getInteger("PotionEffect") % EnumPotionType.values().length]; + potionType = EnumPotionType.fromOrdinal(compound.getInteger("PotionEffect")); potionDuration = compound.getInteger("PotionDuration"); potionAmp = compound.getInteger("PotionAmp"); + if (potionType == EnumPotionType.Manual) + potionManualId = compound.getInteger("PotionManualId"); rangedRange = compound.getInteger("MaxFiringRange"); fireRate = compound.getInteger("FireRate"); @@ -180,7 +188,7 @@ public void readToNBT(NBTTagCompound compound) { pRender3D = compound.getBoolean("pRender3D"); pSpin = compound.getBoolean("pSpin"); pStick = compound.getBoolean("pStick"); - pEffect = EnumPotionType.values()[compound.getInteger("pEffect") % EnumPotionType.values().length]; + pEffect = EnumPotionType.fromOrdinal(compound.getInteger("pEffect")); pTrail = EnumParticleType.values()[compound.getInteger("pTrail") % EnumParticleType.values().length]; pEffAmp = compound.getInteger("pEffAmp"); fireSound = compound.getString("FiringSound"); @@ -195,6 +203,8 @@ public void readToNBT(NBTTagCompound compound) { pCustom = ScriptParticle.fromNBT(compound.getCompoundTag("pCustom")); if (pEffect == EnumPotionType.Fire) pBurnItem = compound.getBoolean("pBurnItem"); + if (pEffect == EnumPotionType.Manual) + pManualId = compound.getInteger("pManualId"); immuneToFire = compound.getBoolean("ImmuneToFire"); potionImmune = compound.getBoolean("PotionImmune"); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java index a128d3760..36cbfff37 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcMeleeProperties.java @@ -12,7 +12,6 @@ public class SubGuiNpcMeleeProperties extends SubGuiInterface implements ITextfieldListener { private DataStats stats; - private String[] potionNames = new String[]{"gui.none", "tile.fire.name", "potion.poison", "potion.hunger", "potion.weakness", "potion.moveSlowdown", "potion.confusion", "potion.blindness", "potion.wither"}; public SubGuiNpcMeleeProperties(DataStats stats) { this.stats = stats; @@ -42,18 +41,28 @@ public void initGui() { getTextField(4).integersOnly = true; getTextField(4).setMinMaxDefault(0, Integer.MAX_VALUE, 0); addLabel(new GuiNpcLabel(5, "stats.meleeeffect", guiLeft + 5, guiTop + 135)); - addButton(new GuiButtonBiDirectional(5, guiLeft + 85, guiTop + 130, 100, 20, potionNames, stats.potionType.ordinal())); + addButton(new GuiButtonBiDirectional(5, guiLeft + 85, guiTop + 130, 100, 20, EnumPotionType.getLangKeys(), stats.potionType.ordinal())); + if (stats.potionType == EnumPotionType.Manual) { + addLabel(new GuiNpcLabel(8, "effect.potionid", guiLeft + 198, guiTop + 119)); + addTextField(new GuiNpcTextField(8, this, fontRendererObj, guiLeft + 200, guiTop + 132, 40, 18, stats.potionManualId + "")); + getTextField(8).integersOnly = true; + getTextField(8).setMinMaxDefault(0, Integer.MAX_VALUE, 0); + } + + int y = guiTop + 160; if (stats.potionType != EnumPotionType.None) { - addLabel(new GuiNpcLabel(6, "gui.time", guiLeft + 5, guiTop + 165)); - addTextField(new GuiNpcTextField(6, this, fontRendererObj, guiLeft + 85, guiTop + 160, 40, 18, stats.potionDuration + "")); + addLabel(new GuiNpcLabel(6, "gui.time", guiLeft + 5, y + 5)); + addTextField(new GuiNpcTextField(6, this, fontRendererObj, guiLeft + 85, y, 40, 18, stats.potionDuration + "")); getTextField(6).integersOnly = true; getTextField(6).setMinMaxDefault(1, Integer.MAX_VALUE, 5); if (stats.potionType != EnumPotionType.Fire) { - addLabel(new GuiNpcLabel(7, "stats.amplify", guiLeft + 5, guiTop + 195)); - addButton(new GuiButtonBiDirectional(7, guiLeft + 85, guiTop + 190, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.potionAmp)); + y += 30; + addLabel(new GuiNpcLabel(7, "stats.amplify", guiLeft + 5, y + 5)); + addButton(new GuiButtonBiDirectional(7, guiLeft + 85, y, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.potionAmp)); } } - addButton(new GuiNpcButton(66, guiLeft + 190, guiTop + 190, 60, 20, "gui.done")); + + addButton(new GuiNpcButton(66, guiLeft + 190, guiTop + ySize - 26, 60, 20, "gui.done")); } public void unFocused(GuiNpcTextField textfield) { @@ -67,13 +76,15 @@ public void unFocused(GuiNpcTextField textfield) { stats.knockback = textfield.getInteger(); } else if (textfield.id == 6) { stats.potionDuration = textfield.getInteger(); + } else if (textfield.id == 8) { + stats.potionManualId = textfield.getInteger(); } } protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; if (button.id == 5) { - stats.potionType = EnumPotionType.values()[button.getValue()]; + stats.potionType = EnumPotionType.fromOrdinal(button.getValue()); initGui(); } if (button.id == 7) { diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java index 8acb8fc8c..e0806a538 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcProjectiles.java @@ -15,7 +15,6 @@ public class SubGuiNpcProjectiles extends SubGuiInterface implements ITextfieldListener, ISubGuiListener { private DataStats stats; - private String[] potionNames = new String[]{"gui.none", "tile.fire.name", "potion.poison", "potion.hunger", "potion.weakness", "potion.moveSlowdown", "potion.confusion", "potion.blindness", "potion.wither"}; private String[] trailNames = new String[]{"gui.none", "trail.smoke", "trail.portal", "trail.redstone", "trail.lightning", "trail.largesmoke", "trail.magic", "trail.enchant", "trail.crit", "trail.explode", "trail.music", "trail.flame", "trail.lava", "trail.splash", @@ -76,22 +75,29 @@ public void initGui() { } addLabel(new GuiNpcLabel(7, "stats.rangedeffect", guiLeft + 210, y + 5)); - addButton(new GuiButtonBiDirectional(4, guiLeft + 280, y, 100, 20, potionNames, stats.pEffect.ordinal())); + addButton(new GuiButtonBiDirectional(4, guiLeft + 280, y, 100, 20, EnumPotionType.getLangKeys(), stats.pEffect.ordinal())); if (stats.pEffect != EnumPotionType.None) { int internalY = y + 30; + if (stats.pEffect == EnumPotionType.Manual) { + addLabel(new GuiNpcLabel(110, "effect.potionid", guiLeft + 210, internalY + 5)); + addTextField(new GuiNpcTextField(11, this, fontRendererObj, guiLeft + 330, internalY, 52, 20, stats.pManualId + "")); + getTextField(11).integersOnly = true; + getTextField(11).setMinMaxDefault(0, Integer.MAX_VALUE, 0); + internalY += 30; + } addLabel(new GuiNpcLabel(50, "gui.time", guiLeft + 210, internalY + 5)); addTextField(new GuiNpcTextField(5, this, fontRendererObj, guiLeft + 330, internalY, 52, 20, stats.pDur + "")); getTextField(5).integersOnly = true; getTextField(5).setMinMaxDefault(1, Integer.MAX_VALUE, 5); - if (stats.pEffect != EnumPotionType.Fire) { - internalY += 30; - addLabel(new GuiNpcLabel(70, "stats.amplify", guiLeft + 210, internalY + 5)); - addButton(new GuiButtonBiDirectional(10, guiLeft + 280, internalY, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.pEffAmp)); - } else { + if (stats.pEffect == EnumPotionType.Fire) { internalY += 30; addLabel(new GuiNpcLabel(100, "stats.burnItem", guiLeft + 210, internalY + 5)); addButton(new GuiNpcButtonYesNo(100, guiLeft + 330, internalY, 52, 20, stats.pBurnItem)); + } else { + internalY += 30; + addLabel(new GuiNpcLabel(70, "stats.amplify", guiLeft + 210, internalY + 5)); + addButton(new GuiButtonBiDirectional(10, guiLeft + 280, internalY, 52, 20, new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, stats.pEffAmp)); } } @@ -126,6 +132,8 @@ public void unFocused(GuiNpcTextField textfield) { stats.pSpeed = textfield.getInteger(); } else if (textfield.id == 5) { stats.pDur = textfield.getInteger(); + } else if (textfield.id == 11) { + stats.pManualId = textfield.getInteger(); } } @@ -146,7 +154,7 @@ protected void actionPerformed(GuiButton guibutton) { stats.pArea = button.getValue(); } if (button.id == 4) { - stats.pEffect = EnumPotionType.values()[button.getValue()]; + stats.pEffect = EnumPotionType.fromOrdinal(button.getValue()); initGui(); } if (button.id == 5) { diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index b5847d0a0..658cfc022 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -180,7 +180,7 @@ public void mouseClicked(int i, int j, int k) { return; } } - + for (GuiNpcTextField tf : new ArrayList(textfields.values())) if (tf.enabled) tf.mouseClicked(i, j, k); @@ -415,7 +415,7 @@ public void drawScreen(int i, int j, float f) { if (tf.hasHoverText()) tf.drawHover(i, j, subGui); } - + for (GuiScreen gui : extra.values()) gui.drawScreen(i, j, f); @@ -439,8 +439,7 @@ protected void drawBackground() { int bottomHeight = 0; int bottomTextureV = 0; if (ySize < bgTextureHeight) { - // Keep bottom border from texture bottom (last 6 pixels of actual content) - bottomHeight = 6; + bottomHeight = -1; topHeight = ySize - bottomHeight; bottomTextureV = bgTextureHeight - bottomHeight; } diff --git a/src/main/java/noppes/npcs/constants/EnumPotionType.java b/src/main/java/noppes/npcs/constants/EnumPotionType.java index f6d5ad111..4cf6bc8f8 100644 --- a/src/main/java/noppes/npcs/constants/EnumPotionType.java +++ b/src/main/java/noppes/npcs/constants/EnumPotionType.java @@ -1,5 +1,83 @@ package noppes.npcs.constants; +import net.minecraft.potion.Potion; + public enum EnumPotionType { - None, Fire, Poison, Hunger, Weakness, Slowness, Nausea, Blindness, Wither; + None("gui.none", -1), + Fire("tile.fire.name", -1), + Poison("potion.poison", Potion.poison.id), + Hunger("potion.hunger", Potion.hunger.id), + Weakness("potion.weakness", Potion.weakness.id), + Slowness("potion.moveSlowdown", Potion.moveSlowdown.id), + Nausea("potion.confusion", Potion.confusion.id), + Blindness("potion.blindness", Potion.blindness.id), + Wither("potion.wither", Potion.wither.id), + MiningFatigue("potion.digSlowDown", Potion.digSlowdown.id), + Manual("effect.manual", -1); + + private final String langKey; + private final int potionId; + + EnumPotionType(String langKey, int potionId) { + this.langKey = langKey; + this.potionId = potionId; + } + + public String getLangKey() { + return langKey; + } + + public int getPotionId() { + return potionId; + } + + /** + * Returns the actual potion ID to apply. + * For Manual type, returns the provided manualId. + * For all others, returns the mapped potionId. + */ + public int getResolvedPotionId(int manualId) { + if (this == Manual) return manualId; + return potionId; + } + + public static String[] getLangKeys() { + EnumPotionType[] types = values(); + String[] keys = new String[types.length]; + for (int i = 0; i < types.length; i++) keys[i] = types[i].langKey; + return keys; + } + + /** + * Returns lang keys excluding None. For use in effect lists where None is not a valid choice. + */ + public static String[] getLangKeysNoNone() { + EnumPotionType[] types = values(); + String[] keys = new String[types.length - 1]; + for (int i = 1; i < types.length; i++) keys[i - 1] = types[i].langKey; + return keys; + } + + /** + * Maps a 0-based index (excluding None) back to the corresponding EnumPotionType. + * Index 0 = Fire, 1 = Poison, etc. + */ + public static EnumPotionType fromIndexNoNone(int index) { + return fromOrdinal(index + 1); + } + + public static EnumPotionType fromOrdinal(int ordinal) { + EnumPotionType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return None; + } + + /** + * Checks if the given potion ID is valid and registered in the Minecraft potion registry. + */ + public static boolean isValidPotionId(int id) { + return id >= 0 && id < Potion.potionTypes.length && Potion.potionTypes[id] != null; + } } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 6e4b40884..187ce583d 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -384,11 +384,13 @@ public boolean attackEntityAsMob(Entity receiver) { } } - if (stats.potionType != EnumPotionType.None) { - if (stats.potionType != EnumPotionType.Fire) - ((EntityLivingBase) receiver).addPotionEffect(new PotionEffect(this.getPotionEffect(stats.potionType), stats.potionDuration * 20, stats.potionAmp)); - else - receiver.setFire(stats.potionDuration); + if (stats.potionType == EnumPotionType.Fire) { + receiver.setFire(stats.potionDuration); + } else if (stats.potionType != EnumPotionType.None) { + int potionId = stats.potionType.getResolvedPotionId(stats.potionManualId); + if (EnumPotionType.isValidPotionId(potionId)) { + ((EntityLivingBase) receiver).addPotionEffect(new PotionEffect(potionId, stats.potionDuration * 20, stats.potionAmp)); + } } return didAttack; } @@ -1043,29 +1045,6 @@ public float getBlockPathWeight(int par1, int par2, int par3) { return weight; } - /* - * Used for getting the applied potion effect from dataStats. - */ - private int getPotionEffect(EnumPotionType p) { - switch (p) { - case Poison: - return Potion.poison.id; - case Hunger: - return Potion.hunger.id; - case Weakness: - return Potion.weakness.id; - case Slowness: - return Potion.moveSlowdown.id; - case Nausea: - return Potion.confusion.id; - case Blindness: - return Potion.blindness.id; - case Wither: - return Potion.wither.id; - default: - return 0; - } - } @Override public void setAir(int air) { diff --git a/src/main/java/noppes/npcs/entity/EntityProjectile.java b/src/main/java/noppes/npcs/entity/EntityProjectile.java index e25a66083..5d322ade8 100644 --- a/src/main/java/noppes/npcs/entity/EntityProjectile.java +++ b/src/main/java/noppes/npcs/entity/EntityProjectile.java @@ -94,6 +94,7 @@ public class EntityProjectile extends EntityThrowable { public boolean destroyTerrain = true; public int explosiveRadius = 0; public EnumPotionType effect = EnumPotionType.None; + public int manualPotionId = 0; public boolean burnItem = true; public int duration = 5; public int amplify = 0; @@ -508,12 +509,12 @@ protected void onImpact(MovingObjectPosition movingobjectposition) { } } - if (this.effect != EnumPotionType.None && entityliving != null) { - if (this.effect != EnumPotionType.Fire) { - int p = this.getPotionEffect(effect); - entityliving.addPotionEffect(new PotionEffect(p, this.duration * 20, this.amplify)); - } else { - movingobjectposition.entityHit.setFire(duration); + if (this.effect == EnumPotionType.Fire && entityliving != null) { + movingobjectposition.entityHit.setFire(duration); + } else if (this.effect != EnumPotionType.None && entityliving != null) { + int potionId = this.effect.getResolvedPotionId(this.manualPotionId); + if (EnumPotionType.isValidPotionId(potionId)) { + entityliving.addPotionEffect(new PotionEffect(potionId, this.duration * 20, this.amplify)); } } } else if (this.hasGravity() && (this.isArrow() || this.sticksToWalls())) { @@ -637,21 +638,23 @@ protected void onImpact(MovingObjectPosition movingobjectposition) { d1 = 1.0D; } - int i = this.getPotionEffect(effect); + int potionId = this.effect.getResolvedPotionId(this.manualPotionId); - if (Potion.potionTypes[i].isInstant()) { - Potion.potionTypes[i].affectEntity(this.getThrower(), entitylivingbase, this.amplify, d1); - } else { - int j = (int) (d1 * (double) this.duration + 0.5D); + if (EnumPotionType.isValidPotionId(potionId)) { + if (Potion.potionTypes[potionId].isInstant()) { + Potion.potionTypes[potionId].affectEntity(this.getThrower(), entitylivingbase, this.amplify, d1); + } else { + int j = (int) (d1 * (double) this.duration + 0.5D); - if (j > 20) { - entitylivingbase.addPotionEffect(new PotionEffect(i, j, this.amplify)); + if (j > 20) { + entitylivingbase.addPotionEffect(new PotionEffect(potionId, j, this.amplify)); + } } } } } } - this.worldObj.playAuxSFX(2002, (int) Math.round(this.posX), (int) Math.round(this.posY), (int) Math.round(this.posZ), this.getPotionColor(this.effect)); + this.worldObj.playAuxSFX(2002, (int) Math.round(this.posX), (int) Math.round(this.posY), (int) Math.round(this.posZ), this.getPotionColor()); } } @@ -693,6 +696,8 @@ public void writeEntityToNBT(NBTTagCompound par1NBTTagCompound) { par1NBTTagCompound.setBoolean("explosive", explosive); par1NBTTagCompound.setInteger("accuracy", accuracy); par1NBTTagCompound.setInteger("PotionEffect", effect.ordinal()); + if (effect == EnumPotionType.Manual) + par1NBTTagCompound.setInteger("PotionManualId", manualPotionId); par1NBTTagCompound.setString("trail", this.dataWatcher.getWatchableObjectString(22)); par1NBTTagCompound.setByte("Render3D", this.dataWatcher.getWatchableObjectByte(28)); par1NBTTagCompound.setByte("Spins", this.dataWatcher.getWatchableObjectByte(29)); @@ -723,7 +728,9 @@ public void readEntityFromNBT(NBTTagCompound par1NBTTagCompound) { this.accelerate = par1NBTTagCompound.getBoolean("accelerate"); this.explosive = par1NBTTagCompound.getBoolean("explosive"); this.accuracy = par1NBTTagCompound.getInteger("accuracy"); - this.effect = EnumPotionType.values()[par1NBTTagCompound.getInteger("PotionEffect") % EnumPotionType.values().length]; + this.effect = EnumPotionType.fromOrdinal(par1NBTTagCompound.getInteger("PotionEffect")); + if (effect == EnumPotionType.Manual) + this.manualPotionId = par1NBTTagCompound.getInteger("PotionManualId"); this.dataWatcher.updateObject(22, par1NBTTagCompound.getString("trail")); this.dataWatcher.updateObject(23, Integer.valueOf(par1NBTTagCompound.getInteger("size"))); this.dataWatcher.updateObject(24, Byte.valueOf((byte) (par1NBTTagCompound.getBoolean("glows") ? 1 : 0))); @@ -774,46 +781,12 @@ public EntityLivingBase getThrower() { return this.thrower; } - private int getPotionEffect(EnumPotionType p) { - switch (p) { - case Poison: - return Potion.poison.id; - case Hunger: - return Potion.hunger.id; - case Weakness: - return Potion.weakness.id; - case Slowness: - return Potion.moveSlowdown.id; - case Nausea: - return Potion.confusion.id; - case Blindness: - return Potion.blindness.id; - case Wither: - return Potion.wither.id; - default: - return 0; - } - } - - private int getPotionColor(EnumPotionType p) { - switch (p) { - case Poison: - return 32660; - case Hunger: - return 32660; - case Weakness: - return 32696; - case Slowness: - return 32698; - case Nausea: - return 32732; - case Blindness: - return Potion.blindness.id; - case Wither: - return 32732; - default: - return 0; + private int getPotionColor() { + int potionId = this.effect.getResolvedPotionId(this.manualPotionId); + if (EnumPotionType.isValidPotionId(potionId)) { + return Potion.potionTypes[potionId].getLiquidColor(); } + return 0; } public void getStatProperties(DataStats stats) { @@ -823,6 +796,7 @@ public void getStatProperties(DataStats stats) { this.explosive = stats.pExplode; this.explosiveRadius = stats.pArea; this.effect = stats.pEffect; + this.manualPotionId = stats.pManualId; this.burnItem = stats.pBurnItem; this.duration = stats.pDur; this.amplify = stats.pEffAmp; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 3e8fa323a..a29759c31 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2007,6 +2007,8 @@ condition.hover.threshold=Health percentage threshold (1-99%) effect.hover.type=Potion effect type to apply to targets effect.hover.duration=Effect duration in ticks (20 ticks = 1 second) effect.hover.amplifier=Effect strength level (higher = stronger) +effect.manual=Manual +effect.potionid=Potion ID # ========================================= # Auction House From f4d7861a9d3337f49bb9c41643d6a2aaf8588ae5 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 13 Feb 2026 14:52:58 -0500 Subject: [PATCH 331/337] fix: prevent special key detection when a screen is open --- src/main/java/noppes/npcs/client/ClientTickHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 203cef3e2..631ad92b0 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -62,7 +62,7 @@ public void onClientTick(TickEvent.ClientTickEvent event) { if (event.phase == Phase.START) { EntityPlayer player = mc.thePlayer; if (player != null) { - boolean specialKeyDown = ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode()); + boolean specialKeyDown = mc.currentScreen == null && ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode()); if (specialKeyDown != lastSpecialKeyDown) { PlayerData data = CustomNpcs.proxy.getPlayerData(player); if (data != null) { From 09ab6458a14e62535ce59cd267624509ee9db5ce Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 13 Feb 2026 16:28:45 -0500 Subject: [PATCH 332/337] refactor: enhance collision detection for ability movement --- .../data/ability/type/AbilityCharge.java | 27 +++++++++++++++++- .../data/ability/type/AbilityDash.java | 28 ++++++++++++++++++- .../data/ability/type/AbilitySlam.java | 11 ++++---- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 50257e023..7cfb44428 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -258,7 +258,32 @@ private boolean isChargeBlocked(EntityLivingBase caster) { double nextX = chargeDirection.xCoord * chargeSpeed; double nextZ = chargeDirection.zCoord * chargeSpeed; AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ); - return !caster.worldObj.getCollidingBoundingBoxes(caster, nextBox).isEmpty(); + double stepThreshold = nextBox.minY + Math.max(caster.stepHeight, 0.5); + + int x1 = (int) Math.floor(nextBox.minX); + int x2 = (int) Math.floor(nextBox.maxX + 1.0); + int y1 = (int) Math.floor(nextBox.minY) - 1; + int y2 = (int) Math.floor(nextBox.maxY + 1.0); + int z1 = (int) Math.floor(nextBox.minZ); + int z2 = (int) Math.floor(nextBox.maxZ + 1.0); + + java.util.ArrayList collisionBoxes = new java.util.ArrayList<>(); + for (int bx = x1; bx < x2; bx++) { + for (int bz = z1; bz < z2; bz++) { + for (int by = y1; by < y2; by++) { + net.minecraft.block.Block block = caster.worldObj.getBlock(bx, by, bz); + if (!block.getMaterial().blocksMovement()) continue; + collisionBoxes.clear(); + block.addCollisionBoxesToList(caster.worldObj, bx, by, bz, nextBox, collisionBoxes, caster); + for (AxisAlignedBB box : collisionBoxes) { + if (box.maxY > stepThreshold) { + return true; + } + } + } + } + } + return false; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 7e3375ca7..6070342d7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -257,7 +257,33 @@ private boolean isDashBlocked(EntityLivingBase caster) { if (dashDirection == null) return true; double nextX = dashDirection.xCoord * dashSpeed; double nextZ = dashDirection.zCoord * dashSpeed; - return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); + AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ); + double stepThreshold = nextBox.minY + Math.max(caster.stepHeight, 0.5); + + int x1 = (int) Math.floor(nextBox.minX); + int x2 = (int) Math.floor(nextBox.maxX + 1.0); + int y1 = (int) Math.floor(nextBox.minY) - 1; + int y2 = (int) Math.floor(nextBox.maxY + 1.0); + int z1 = (int) Math.floor(nextBox.minZ); + int z2 = (int) Math.floor(nextBox.maxZ + 1.0); + + java.util.ArrayList collisionBoxes = new java.util.ArrayList<>(); + for (int bx = x1; bx < x2; bx++) { + for (int bz = z1; bz < z2; bz++) { + for (int by = y1; by < y2; by++) { + net.minecraft.block.Block block = caster.worldObj.getBlock(bx, by, bz); + if (!block.getMaterial().blocksMovement()) continue; + collisionBoxes.clear(); + block.addCollisionBoxesToList(caster.worldObj, bx, by, bz, nextBox, collisionBoxes, caster); + for (AxisAlignedBB box : collisionBoxes) { + if (box.maxY > stepThreshold) { + return true; + } + } + } + } + } + return false; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 0bd9613a5..31c109ab8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -283,11 +283,12 @@ public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World caster.fallDistance = 0; } - // Check for landing - if (caster.onGround && airTicks > 3) { - // Landed! - onLanding(caster, world); - return; + // Check for landing: hit the ground or hit a wall mid-flight + if (airTicks > 3) { + if (caster.onGround || caster.isCollidedHorizontally) { + onLanding(caster, world); + return; + } } // Timeout protection - force landing after max air time From 0b0dcdc58dfabc6fc89887bc60804a498211e949 Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 14 Feb 2026 03:50:28 -0500 Subject: [PATCH 333/337] feat: implement player ability state management with rotation and position locking --- .../data/ability/type/AbilitySlam.java | 52 ++-- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumDataPacket.java | 1 + .../ability/PlayerAbilityStatePacket.java | 84 +++++++ .../noppes/npcs/ScriptPlayerEventHandler.java | 10 +- .../npcs/client/ClientAbilityState.java | 59 +++++ .../noppes/npcs/client/ClientTickHandler.java | 14 ++ .../controllers/data/PlayerAbilityData.java | 224 +++++++++++++++++- 8 files changed, 426 insertions(+), 20 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java create mode 100644 src/main/java/noppes/npcs/client/ClientAbilityState.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 31c109ab8..22fd99416 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -86,10 +86,20 @@ public boolean hasAbilityMovement() { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (isPlayerCaster(caster)) { - // Player: slam will launch from current position, no target tracking needed - targetX = caster.posX; - targetY = caster.posY; - targetZ = caster.posZ; + // Player: update target along look direction during windup + if (targetingMode == TargetingMode.AOE_SELF) { + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } else { + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + double launchDist = Math.max(4.0, maxRange * 0.5); + targetX = caster.posX + dirX * launchDist; + targetY = caster.posY; + targetZ = caster.posZ + dirZ * launchDist; + } } else { // NPC: update target position during windup for telegraph tracking if (targetingMode == TargetingMode.AOE_SELF) { @@ -139,18 +149,22 @@ private void executeNpcSlam(EntityLivingBase caster, EntityLivingBase target) { } /** - * Player slam: Launch straight up. Player controls horizontal movement via WASD. + * Player slam: Launch in look direction using same ballistic arc as NPC slam. * AOE damage triggers wherever the player lands. */ private void executePlayerSlam(EntityLivingBase caster) { - targetX = caster.posX; + // Calculate target position along player's look direction (horizontal only) + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + + double launchDist = Math.max(4.0, maxRange * 0.5); + + targetX = caster.posX + dirX * launchDist; targetY = caster.posY; - targetZ = caster.posZ; + targetZ = caster.posZ + dirZ * launchDist; - double vy = calculateLaunchVelocity(Math.max(1.0, leapHeight)); - caster.motionY = vy; - hasLaunched = true; - caster.velocityChanged = true; + launchTowardTarget(caster); } /** @@ -297,14 +311,18 @@ public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World return; } - // NPC: face toward target destination while in air - // Player: free look — player controls their own camera - if (!isPreview() && !isPlayerCaster(caster)) { + // Face toward target destination while in air (both NPC and Player) + if (!isPreview()) { double dx = targetX - caster.posX; double dz = targetZ - caster.posZ; - float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - caster.rotationYaw = targetYaw; - caster.rotationYawHead = targetYaw; + if (dx * dx + dz * dz > 0.25) { + float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; + if (isPlayerCaster(caster)) { + caster.velocityChanged = true; + } + } } } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 6db07c841..dee4a3a94 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,7 @@ import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; @@ -588,6 +589,7 @@ public void registerDataPackets() { DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); + DATA_PACKET.registerPacket(new PlayerAbilityStatePacket()); } public void registerPlayerPackets() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 18321d0a4..8362cb3c5 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -63,6 +63,7 @@ public enum EnumDataPacket { TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, PLAYER_ABILITY_SYNC, + PLAYER_ABILITY_STATE, // Auction System AUCTION_DATA, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java new file mode 100644 index 000000000..03c0b82b4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java @@ -0,0 +1,84 @@ +package kamkeel.npcs.network.packets.data.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumDataPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.client.ClientAbilityState; + +import java.io.IOException; + +/** + * Lightweight packet syncing ability lock state from server to client. + * Sent every tick during ability execution when flags change, and once when ability ends. + * + * Flags byte layout: + * bit 0: movement locked (suppress WASD, zero motion) + * bit 1: rotation locked (freeze yaw/pitch) + * bit 2: has ability movement (ability controls motion — suppress WASD but don't zero) + * bit 3: position locked (freeze position) + */ +public final class PlayerAbilityStatePacket extends AbstractPacket { + public static final String packetName = "Data|PlayerAbilityState"; + + public static final byte FLAG_MOVEMENT_LOCKED = 1; + public static final byte FLAG_ROTATION_LOCKED = 2; + public static final byte FLAG_HAS_ABILITY_MOVEMENT = 4; + public static final byte FLAG_POSITION_LOCKED = 8; + + private byte flags; + private float lockedYaw; + private float lockedPitch; + + public PlayerAbilityStatePacket() { + } + + public PlayerAbilityStatePacket(byte flags, float lockedYaw, float lockedPitch) { + this.flags = flags; + this.lockedYaw = lockedYaw; + this.lockedPitch = lockedPitch; + } + + @Override + public Enum getType() { + return EnumDataPacket.PLAYER_ABILITY_STATE; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeByte(flags); + if ((flags & FLAG_ROTATION_LOCKED) != 0) { + out.writeFloat(lockedYaw); + out.writeFloat(lockedPitch); + } + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + byte flags = in.readByte(); + float yaw = 0, pitch = 0; + if ((flags & FLAG_ROTATION_LOCKED) != 0) { + yaw = in.readFloat(); + pitch = in.readFloat(); + } + ClientAbilityState.update(flags, yaw, pitch); + } + + /** + * Send the current ability state to the player's client. + */ + public static void sendToPlayer(EntityPlayerMP player, byte flags, float lockedYaw, float lockedPitch) { + PacketHandler.Instance.sendToPlayer(new PlayerAbilityStatePacket(flags, lockedYaw, lockedPitch), player); + } +} diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index c77083fa1..909634a09 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -8,6 +8,7 @@ import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.util.AttributeAttackUtil; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -100,9 +101,14 @@ public void onServerTick(TickEvent.PlayerTickEvent event) { playerData.abilityData.tick(player); // Lock player movement during ability phases that require it + // Skip zeroing if the ability has its own movement (Charge/Dash/Slam) if (playerData.abilityData.isMovementLocked()) { - player.motionX = 0; - player.motionZ = 0; + Ability current = playerData.abilityData.getCurrentAbility(); + if (current == null || !current.hasAbilityMovement()) { + player.motionX = 0; + player.motionZ = 0; + player.velocityChanged = true; + } } if (playerData.updateClient) { diff --git a/src/main/java/noppes/npcs/client/ClientAbilityState.java b/src/main/java/noppes/npcs/client/ClientAbilityState.java new file mode 100644 index 000000000..8092a69a3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/ClientAbilityState.java @@ -0,0 +1,59 @@ +package noppes.npcs.client; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; + +/** + * Client-side state holder for player ability lock flags. + * Updated by {@link PlayerAbilityStatePacket} from the server. + * Read by {@link ClientTickHandler} to suppress WASD input and enforce rotation. + */ +@SideOnly(Side.CLIENT) +public class ClientAbilityState { + public static boolean movementLocked = false; + public static boolean rotationLocked = false; + public static boolean hasAbilityMovement = false; + public static boolean positionLocked = false; + public static float lockedYaw = 0; + public static float lockedPitch = 0; + + /** + * Update state from a received packet. + */ + public static void update(byte flags, float yaw, float pitch) { + movementLocked = (flags & PlayerAbilityStatePacket.FLAG_MOVEMENT_LOCKED) != 0; + rotationLocked = (flags & PlayerAbilityStatePacket.FLAG_ROTATION_LOCKED) != 0; + hasAbilityMovement = (flags & PlayerAbilityStatePacket.FLAG_HAS_ABILITY_MOVEMENT) != 0; + positionLocked = (flags & PlayerAbilityStatePacket.FLAG_POSITION_LOCKED) != 0; + lockedYaw = yaw; + lockedPitch = pitch; + } + + /** + * Whether player WASD movement input should be suppressed. + * True when movement/position is locked or ability has its own movement. + */ + public static boolean shouldSuppressMovementInput() { + return movementLocked || hasAbilityMovement || positionLocked; + } + + /** + * Whether player camera rotation should be locked. + */ + public static boolean shouldLockRotation() { + return rotationLocked; + } + + /** + * Reset all state. Called on world change / disconnect. + */ + public static void reset() { + movementLocked = false; + rotationLocked = false; + hasAbilityMovement = false; + positionLocked = false; + lockedYaw = 0; + lockedPitch = 0; + } +} diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 631ad92b0..4c6b7a044 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -56,6 +56,7 @@ public void onClientTick(TickEvent.ClientTickEvent event) { if ((this.prevWorld == null || mc.theWorld == null) && this.prevWorld != mc.theWorld) { if (mc.theWorld == null) { ClientCacheHandler.clearCache(); + ClientAbilityState.reset(); } this.prevWorld = mc.theWorld; } @@ -72,6 +73,19 @@ public void onClientTick(TickEvent.ClientTickEvent event) { lastSpecialKeyDown = specialKeyDown; } + // Suppress player input during ability-controlled phases + if (ClientAbilityState.shouldSuppressMovementInput()) { + mc.thePlayer.movementInput.moveForward = 0; + mc.thePlayer.movementInput.moveStrafe = 0; + mc.thePlayer.movementInput.jump = false; + } + if (ClientAbilityState.shouldLockRotation()) { + mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw; + mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch; + mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw; + mc.thePlayer.prevRotationPitch = ClientAbilityState.lockedPitch; + } + if (player.ridingEntity instanceof EntityNPCInterface) { EntityNPCInterface mount = (EntityNPCInterface) player.ridingEntity; if (mount.advanced.role == EnumRoleType.Mount && mount.roleInterface instanceof RoleMount) { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 3e6489b1c..7807074f3 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -19,6 +19,7 @@ import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import java.util.ArrayList; @@ -77,6 +78,26 @@ public class PlayerAbilityData implements IPlayerAbilityData { */ private transient EntityLivingBase currentTarget; + /** + * Rotation lock state - freezes player yaw/pitch during ability phases. + */ + private transient boolean rotationLocked = false; + private transient float lockedYaw = 0; + private transient float lockedPitch = 0; + + /** + * Position lock state - freezes player position during ability phases. + */ + private transient boolean positionLocked = false; + private transient double lockedPosX = 0; + private transient double lockedPosY = 0; + private transient double lockedPosZ = 0; + + /** + * Last synced state flags byte for change detection (avoids spamming packets). + */ + private transient byte lastSyncedFlags = 0; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -100,6 +121,20 @@ public void tick(EntityPlayer player) { if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(player); + + // Apply rotation and position locks after ability tick + applyRotationControl(player); + applyPositionLock(player); + + // Sync lock state to client (only sends when flags change) + syncAbilityStateIfNeeded(player); + } else { + // Safety: release orphaned locks if no ability is executing + if (rotationLocked || positionLocked) { + releaseRotationControl(); + releaseLockedPosition(); + syncAbilityStateClear(player); + } } } @@ -114,17 +149,47 @@ private void tickCurrentAbility(EntityPlayer player) { switch (currentAbility.getPhase()) { case WINDUP: + if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { + // Burst replay: re-enter windup - set up locks + if (currentAbility.isRotationLockedDuringWindup()) { + captureLockedRotation(player); + } + if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) { + captureLockedPosition(player); + } + spawnTelegraph(currentAbility, player, null); + playAbilitySound(player, currentAbility.getWindUpSound()); + playAbilityAnimation(currentAbility.getWindUpAnimation()); + } currentAbility.onWindUpTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); break; case ACTIVE: - if (phaseChanged && oldPhase == AbilityPhase.WINDUP) { + if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) { // Just entered ACTIVE phase - lock all telegraph positions for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) { telegraph.lockPosition(); } removeTelegraph(currentAbility, player); + // Handle rotation control transition from WINDUP to ACTIVE + if (currentAbility.isRotationLockedDuringActive()) { + if (!rotationLocked) { + captureLockedRotation(player); + } + } else if (rotationLocked) { + releaseRotationControl(); + } + + // Handle position lock transition from WINDUP to ACTIVE + if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) { + if (!positionLocked) { + captureLockedPosition(player); + } + } else if (positionLocked) { + releaseLockedPosition(); + } + // Play active sound and animation playAbilitySound(player, currentAbility.getActiveSound()); playAbilityAnimation(currentAbility.getActiveAnimation()); @@ -143,6 +208,18 @@ private void tickCurrentAbility(EntityPlayer player) { handleAbilityCompletion(player); return; } + + // Release locks during burst delay + if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) { + if (rotationLocked) releaseRotationControl(); + if (positionLocked) releaseLockedPosition(); + } + break; + + case BURST_DELAY: + // Free movement and rotation during burst delay + if (rotationLocked) releaseRotationControl(); + if (positionLocked) releaseLockedPosition(); break; case DAZED: @@ -163,6 +240,10 @@ private void handleAbilityCompletion(EntityPlayer player) { currentAbility.onComplete(player, currentTarget); + // Release all locks + releaseRotationControl(); + releaseLockedPosition(); + // Apply universal cooldown (using base cooldownTicks only, not min/max random) if (!currentAbility.isIgnoreCooldown()) { cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks(); @@ -174,6 +255,9 @@ private void handleAbilityCompletion(EntityPlayer player) { currentAbility = null; currentAbilityKey = null; currentTarget = null; + + // Send cleared state to client + syncAbilityStateClear(player); } // ═══════════════════════════════════════════════════════════════════ @@ -231,9 +315,21 @@ public boolean activateAbility(EntityPlayer player, String key) { if (ability.getPhase() == AbilityPhase.ACTIVE) { // Windup was 0 — skip telegraph/windup and go straight to active + if (ability.isRotationLockedDuringActive()) { + captureLockedRotation(player); + } + if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) { + captureLockedPosition(player); + } executeImmediate(ability, player); } else { // Normal windup flow + if (ability.isRotationLockedDuringWindup()) { + captureLockedRotation(player); + } + if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) { + captureLockedPosition(player); + } spawnTelegraph(ability, player, null); playAbilitySound(player, ability.getWindUpSound()); playAbilityAnimation(ability.getWindUpAnimation()); @@ -412,9 +508,12 @@ public void interruptCurrentAbility() { if (currentAbility != null && currentAbility.isExecuting()) { currentAbility.interrupt(); stopAbilityAnimation(); + releaseRotationControl(); + releaseLockedPosition(); currentAbility = null; currentAbilityKey = null; currentTarget = null; + syncAbilityStateClear(playerData.player); } } @@ -493,6 +592,129 @@ public boolean isMovementLocked() { && currentAbility.isMovementLockedForCurrentPhase(); } + // ═══════════════════════════════════════════════════════════════════ + // ROTATION CONTROL + // ═══════════════════════════════════════════════════════════════════ + + /** + * Capture current rotation values to lock player's look direction. + */ + private void captureLockedRotation(EntityPlayer player) { + lockedYaw = player.rotationYaw; + lockedPitch = player.rotationPitch; + rotationLocked = true; + } + + /** + * Release rotation lock. + */ + private void releaseRotationControl() { + rotationLocked = false; + } + + /** + * Apply rotation control on the server side. + * Enforces locked rotation values on the player entity. + */ + private void applyRotationControl(EntityPlayer player) { + if (!rotationLocked || currentAbility == null) return; + + if (currentAbility.isRotationLockedForCurrentPhase()) { + player.rotationYaw = lockedYaw; + player.rotationPitch = lockedPitch; + player.prevRotationYaw = lockedYaw; + player.prevRotationPitch = lockedPitch; + if (player instanceof EntityPlayerMP) { + player.rotationYawHead = lockedYaw; + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // POSITION LOCKING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Capture current position to lock player in place. + */ + private void captureLockedPosition(EntityPlayer player) { + lockedPosX = player.posX; + lockedPosY = player.posY; + lockedPosZ = player.posZ; + positionLocked = true; + } + + /** + * Release position lock. + */ + private void releaseLockedPosition() { + positionLocked = false; + } + + /** + * Apply position lock on the server side. + * Snaps player back to locked position and zeroes motion. + */ + private void applyPositionLock(EntityPlayer player) { + if (!positionLocked) return; + + player.setPosition(lockedPosX, lockedPosY, lockedPosZ); + player.prevPosX = lockedPosX; + player.prevPosY = lockedPosY; + player.prevPosZ = lockedPosZ; + player.motionX = 0; + player.motionZ = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY STATE SYNC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Build the current ability state flags byte. + */ + private byte getAbilityStateFlags() { + byte flags = 0; + if (currentAbility == null || !currentAbility.isExecuting()) return flags; + + if (currentAbility.isMovementLockedForCurrentPhase()) { + flags |= PlayerAbilityStatePacket.FLAG_MOVEMENT_LOCKED; + } + if (rotationLocked && currentAbility.isRotationLockedForCurrentPhase()) { + flags |= PlayerAbilityStatePacket.FLAG_ROTATION_LOCKED; + } + if (currentAbility.hasAbilityMovement() && currentAbility.getPhase() == AbilityPhase.ACTIVE) { + flags |= PlayerAbilityStatePacket.FLAG_HAS_ABILITY_MOVEMENT; + } + if (positionLocked) { + flags |= PlayerAbilityStatePacket.FLAG_POSITION_LOCKED; + } + return flags; + } + + /** + * Send ability state to client if flags changed since last sync. + */ + private void syncAbilityStateIfNeeded(EntityPlayer player) { + if (!(player instanceof EntityPlayerMP)) return; + byte flags = getAbilityStateFlags(); + if (flags != lastSyncedFlags) { + lastSyncedFlags = flags; + PlayerAbilityStatePacket.sendToPlayer((EntityPlayerMP) player, flags, lockedYaw, lockedPitch); + } + } + + /** + * Send cleared (all-zero) state to client. Called on ability completion/interrupt. + */ + private void syncAbilityStateClear(EntityPlayer player) { + if (!(player instanceof EntityPlayerMP)) return; + if (lastSyncedFlags != 0) { + lastSyncedFlags = 0; + PlayerAbilityStatePacket.sendToPlayer((EntityPlayerMP) player, (byte) 0, 0, 0); + } + } + // ═══════════════════════════════════════════════════════════════════ // TELEGRAPH // ═══════════════════════════════════════════════════════════════════ From 9fcda980ee56cab60a69178184612eadf69c4683 Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 14 Feb 2026 03:57:40 -0500 Subject: [PATCH 334/337] fix: convert gradle-plugins from directory to submodule Matches dev branch's submodule reference so branch switching works seamlessly. Co-Authored-By: Claude Opus 4.6 --- gradle-plugins | 1 + gradle-plugins/.gitignore | 9 - gradle-plugins/README.md | 94 -- gradle-plugins/build.gradle | 50 - gradle-plugins/settings.gradle | 1 - .../groovy/dts/GenerateTypeScriptTask.groovy | 175 --- .../dts/JavaToTypeScriptConverter.groovy | 1353 ----------------- .../dts/TypeScriptGeneratorPlugin.groovy | 17 - .../dts.typescript-generator.properties | 1 - 9 files changed, 1 insertion(+), 1700 deletions(-) create mode 160000 gradle-plugins delete mode 100644 gradle-plugins/.gitignore delete mode 100644 gradle-plugins/README.md delete mode 100644 gradle-plugins/build.gradle delete mode 100644 gradle-plugins/settings.gradle delete mode 100644 gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy delete mode 100644 gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy delete mode 100644 gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy delete mode 100644 gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties diff --git a/gradle-plugins b/gradle-plugins new file mode 160000 index 000000000..f1c82c10c --- /dev/null +++ b/gradle-plugins @@ -0,0 +1 @@ +Subproject commit f1c82c10c8c92eaaf55044cf931da5e1c7a2668f diff --git a/gradle-plugins/.gitignore b/gradle-plugins/.gitignore deleted file mode 100644 index 22bff6717..000000000 --- a/gradle-plugins/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/.gradle -/build -/out -/bin -/.idea -/.vscode -/.settings -.project -.classpath \ No newline at end of file diff --git a/gradle-plugins/README.md b/gradle-plugins/README.md deleted file mode 100644 index 1e0e7fba2..000000000 --- a/gradle-plugins/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# dts-gradle-plugin - -This repo contains the Gradle plugin for generating TypeScript `.d.ts` files for the CustomNPC-Plus API. - -These files are used for the generation of auto-completion suggestions and type/interface documentation when scripting -in the Script Editor in-game. - -### Implementation using JitPack - -Add JitPack to the buildscript and depend on the plugin JAR. Using `main-SNAPSHOT` will build the latest `main` commit; -for reproducible builds use a tag or commit hash instead. - -`settings.gradle` -```gradle -pluginManagement { - resolutionStrategy { - eachPlugin { - if(requested.id.toString() == "dts.typescript-generator") { - useModule("com.github.bigguy345:dts-gradle-plugin:main-SNAPSHOT") - } - } - } - - - repositories { - maven { url "https://jitpack.io" } - gradlePluginPortal() - mavenCentral() - mavenLocal() - } -} -``` - - - - -`build.gradle`: - -```gradle -plugins { - id 'dts.typescript-generator' -} - -// ============================================================================ -// TypeScript Definition Generation Task -// Generates .d.ts files from Java API sources for scripting IDE support -// ============================================================================ -// TypeScript plugin is applied above in the main plugins block - -tasks.named("generateTypeScriptDefinitions").configure { - // Source directories containing the Java API code - sourceDirectories = ['src/main/java'] - - // Packages in source directories to generate .d.ts files for - apiPackages = ['noppes.npcs.api'] as Set - - // Output directory for the generated .d.ts files - // Must be within resources/assets/${modId}/api to be detected by CNPC+ - outputDirectory = "src/main/resources/assets/${modId}/api" - - // Whether to clean old generated files before regenerating - cleanOutputFirst = true - - // Optional: copy external patch .d.ts files into assets//api/patches - patchesDirectory = "dts-patches" -} - - -// Optional: To ensure definitions are generated on processing resources on jar build -// But in most cases, you may want to run the task manually when needed -// processResources.dependsOn generateTypeScriptDefinitions -``` - ---- - -## Patches (optional overrides) - -Use `dts-patches/` to override or refine generated types. Files in this folder are copied to -`assets//api/patches` after generation. - -### Example: override `IPlayer.getDBCPlayer()` to return `IDBCAddon` instead of `IDBCPlayer` - -File: `dts-patches/IPlayer.d.ts` - -```ts -/** - * DBC Addon patch for IPlayer - * @javaFqn noppes.npcs.api.entity.IPlayer - */ -export interface IPlayer { - getDBCPlayer(): IDBCAddon; -} -``` - diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle deleted file mode 100644 index 0deda7acc..000000000 --- a/gradle-plugins/build.gradle +++ /dev/null @@ -1,50 +0,0 @@ -plugins { - id 'groovy-gradle-plugin' - id 'maven-publish' -} - -group = 'dts' -version = '1.0.0' - -repositories { - mavenCentral() -} - -dependencies { - implementation gradleApi() - implementation localGroovy() -} - -// Configure source sets if building as standalone -sourceSets { - main { - groovy { - srcDirs = ['src/main/groovy'] - } - resources { - srcDirs = ['src/main/resources'] - } - } -} - -gradlePlugin { - plugins { - typescriptGenerator { - id = 'dts.typescript-generator' - implementationClass = 'dts.TypeScriptGeneratorPlugin' - displayName = 'TypeScript Definition Generator' - description = 'Generates TypeScript definition files from Java API sources' - } - } -} - -publishing { - repositories { - mavenLocal() - } -} - -// Handle duplicate resources -tasks.processResources { - duplicatesStrategy = 'include' -} diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle deleted file mode 100644 index 14cad52d4..000000000 --- a/gradle-plugins/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'dts-gradle-plugin' diff --git a/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy b/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy deleted file mode 100644 index b62e6cedc..000000000 --- a/gradle-plugins/src/main/groovy/dts/GenerateTypeScriptTask.groovy +++ /dev/null @@ -1,175 +0,0 @@ -package dts - -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.Internal -import org.gradle.api.tasks.Optional - -/** - * Gradle task that generates TypeScript definition (.d.ts) files from Java API sources. - * - * Usage in build.gradle: - * - * tasks.withType(GenerateTypeScriptTask).configureEach { - * sourceDirectories = ['src/api/java'] // Strings are converted to Files - * outputDirectory = 'src/main/resources/${modid}/api' // String converted to File - * apiPackages = ['noppes.npcs.api', 'kamkeel.npcdbc.api'] - * } - */ -abstract class GenerateTypeScriptTask extends DefaultTask { - - // Accept Object types (String or File) with @Internal annotation - @Internal - List sourceDirectories = [] - - @Internal - Object outputDirectory - - @Input - Set apiPackages = [] - - @Input - Boolean cleanOutputFirst = false - - @Input - List excludePatterns = [] - - @Internal - Object patchesDirectory - - // Provide task input/output properties that Gradle can validate - @InputFiles - protected List getResolvedSourceDirectories() { - return sourceDirectories.collect { obj -> - if (obj instanceof File) return obj - String pathStr = obj.toString() - if (pathStr.contains('${modid}')) { - String modid = project.archivesBaseName ?: 'mod' - pathStr = pathStr.replace('${modid}', modid) - } - File f = new File(pathStr) - if (!f.isAbsolute()) { - f = new File(project.projectDir, pathStr) - } - return f - } - } - - @OutputDirectory - protected File getResolvedOutputDirectory() { - def obj = outputDirectory - if (obj instanceof File) return obj - String pathStr = obj.toString() - if (pathStr.contains('${modid}')) { - String modid = project.archivesBaseName ?: 'mod' - pathStr = pathStr.replace('${modid}', modid) - } - File f = new File(pathStr) - if (!f.isAbsolute()) { - f = new File(project.projectDir, pathStr) - } - return f - } - - @Optional - @InputDirectory - protected File getResolvedDtsPatchesDirectory() { - if (patchesDirectory == null) return null - def obj = patchesDirectory - if (obj instanceof File) return obj - String pathStr = obj.toString() - if (pathStr.contains('${modid}')) { - String modid = project.archivesBaseName ?: 'mod' - pathStr = pathStr.replace('${modid}', modid) - } - File f = new File(pathStr) - if (!f.isAbsolute()) { - f = new File(project.projectDir, pathStr) - } - return f - } - - /** - * Converts a value (String or File) to a File object. - * Supports special tokens like ${modid}. - */ - // conversion logic inlined into getters to avoid calling private helper at configuration time - - GenerateTypeScriptTask() { - group = 'api' - description = 'Generates TypeScript definition files from Java API sources' - } - - @TaskAction - void generate() { - List srcDirs = getResolvedSourceDirectories() - File outDir = getResolvedOutputDirectory() - File patchesDir = getResolvedDtsPatchesDirectory() - - logger.lifecycle("=".multiply(60)) - logger.lifecycle("Generating TypeScript definitions...") - logger.lifecycle("=".multiply(60)) - - if (srcDirs.isEmpty()) { - logger.warn("No source directories specified!") - return - } - - // Validate directories - srcDirs.each { dir -> - if (!dir.exists()) { - logger.warn("Source directory does not exist: ${dir}") - } else { - logger.lifecycle("Source: ${dir}") - } - } - - logger.lifecycle("Output: ${outDir}") - logger.lifecycle("API Packages: ${apiPackages}") - - // Clean output if requested - if (cleanOutputFirst && outDir.exists()) { - logger.lifecycle("Cleaning output directory...") - outDir.eachFileRecurse { file -> - if (file.name.endsWith('.d.ts') && !file.name.equals('minecraft-raw.d.ts') && !file.name.equals('forge-events-raw.d.ts')) { - file.delete() - } - } - } - - // Create converter and process - JavaToTypeScriptConverter converter = new JavaToTypeScriptConverter(outDir, apiPackages) - - List validDirs = srcDirs.findAll { it.exists() } - if (validDirs.isEmpty()) { - logger.error("No valid source directories found!") - return - } - - converter.processDirectories(validDirs, logger) - - if (patchesDir != null) { - if (!patchesDir.exists()) { - logger.lifecycle("Patches directory does not exist: ${patchesDir}") - } else { - File patchOutputDir = new File(outDir, "patches") - if (patchOutputDir.exists()) { - patchOutputDir.deleteDir() - } - project.copy { - from patchesDir - into patchOutputDir - } - logger.lifecycle("Copied .d.ts patches to: ${patchOutputDir}") - } - } - - logger.lifecycle("=".multiply(60)) - logger.lifecycle("TypeScript definition generation complete!") - logger.lifecycle("=".multiply(60)) - } -} diff --git a/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy b/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy deleted file mode 100644 index 9d96633ed..000000000 --- a/gradle-plugins/src/main/groovy/dts/JavaToTypeScriptConverter.groovy +++ /dev/null @@ -1,1353 +0,0 @@ -package dts - -import groovy.transform.TypeChecked -import org.gradle.api.logging.Logger - -import java.util.regex.Pattern -import java.util.regex.Matcher - -/** - * Parses Java source files and converts them to TypeScript definition (.d.ts) files. - * Handles interfaces, classes, nested types, generics, and JavaDoc preservation. - */ -class JavaToTypeScriptConverter { - - // Common Java type mappings to TypeScript - private static final Map PRIMITIVE_MAPPINGS = [ - 'void': 'void', - 'boolean': 'boolean', - 'byte': 'number', - 'short': 'number', - 'int': 'number', - 'long': 'number', - 'float': 'number', - 'double': 'number', - 'char': 'string', - 'String': 'string', - 'Object': 'any', - 'Boolean': 'boolean', - 'Byte': 'number', - 'Short': 'number', - 'Integer': 'number', - 'Long': 'number', - 'Float': 'number', - 'Double': 'number', - 'Character': 'string', - 'Number': 'number', - ] - - // Java functional interface mappings - private static final Map FUNCTIONAL_MAPPINGS = [ - 'Consumer': '(arg: %s) => void', - 'Supplier': '() => %s', - 'Function': '(arg: %s) => %s', - 'Predicate': '(arg: %s) => boolean', - 'BiConsumer': '(arg1: %s, arg2: %s) => void', - 'BiFunction': '(arg1: %s, arg2: %s) => %s', - 'Runnable': '() => void', - 'Callable': '() => %s', - ] - - // Packages that are part of API (generate imports to local .d.ts) - private Set apiPackages = [] as Set - - // Base output directory for generated files - private File outputDir - - // Track all generated types for index.d.ts - 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) { - this.outputDir = outputDir - this.apiPackages = apiPackages - } - - /** - * Process all Java files in the given directories - */ - void processDirectories(List sourceDirs, Logger logger) { - this.logger = logger - sourceDirs.each { dir -> - if (dir.exists()) { - processDirectory(dir, dir) - } - } - - // Generate index.d.ts - generateIndexFile() - - // Generate hooks.d.ts - generateHooksFile() - } - - private void processDirectory(File dir, File baseDir) { - dir.eachFileRecurse { file -> - if (file.name.endsWith('.java') && !file.name.equals('package-info.java')) { - // Early filtering - check if file matches any API package - String relativePath = baseDir.toPath().relativize(file.toPath()).toString() - String packagePath = relativePath.replace('\\', '/').replace('.java', '').replace('/', '.') - - // Only process if the package starts with one of the apiPackages - boolean shouldProcess = apiPackages.any { apiPkg -> packagePath.startsWith(apiPkg) } - - if (shouldProcess) - processJavaFile(file, baseDir) - } - } - } - - - - /** - * Process a single Java file - */ - void processJavaFile(File javaFile, File baseDir) { - String content = javaFile.text - ParsedJavaFile parsed = parseJavaFile(content) - - if (parsed == null) { - logger.warn("Failed to parse ${javaFile.name} - parseJavaFile returned null") - return - } - if (parsed.types.isEmpty()) { - logger.warn("No types extracted from ${javaFile.name} - package: ${parsed.packageName}") - return - } - // logger.lifecycle("Successfully parsed ${javaFile.name}: ${parsed.types.size()} type(s) found: ${parsed.types*.name}") - - // Determine output path - String relativePath = baseDir.toPath().relativize(javaFile.toPath()).toString() - String dtsPath = relativePath.replace('.java', '.d.ts').replace('\\', '/') - File outputFile = new File(outputDir, dtsPath) - - // Generate TypeScript content - String tsContent = generateTypeScript(parsed, dtsPath) - - // Write file - outputFile.parentFile.mkdirs() - outputFile.text = tsContent - - // Track for index generation - parsed.types.each { type -> - generatedTypes << new TypeInfo( - name: type.name, - packageName: parsed.packageName, - filePath: dtsPath, - isClass: type.isClass, - isInterface: type.isInterface, - extendsType: type.extendsType - ) - - // Track nested types - type.nestedTypes.each { nested -> - generatedTypes << new TypeInfo( - name: "${type.name}.${nested.name}", - packageName: parsed.packageName, - filePath: dtsPath, - isClass: nested.isClass, - isInterface: nested.isInterface, - parentType: type.name - ) - } - } - - // Collect hooks from event interfaces - collectHooks(parsed) - } - - /** - * Parse a Java file into structured data - */ - ParsedJavaFile parseJavaFile(String content) { - ParsedJavaFile result = new ParsedJavaFile() - - // Extract package - def packageMatcher = content =~ /package\s+([\w.]+)\s*;/ - if (packageMatcher.find()) { - result.packageName = packageMatcher.group(1) - } - - // Extract imports - def importMatcher = content =~ /import\s+([\w.*]+)\s*;/ - while (importMatcher.find()) { - result.imports << importMatcher.group(1) - } - - // Parse types (interfaces and classes) - parseTypes(content, result) - - return result - } - - private void parseTypes(String content, ParsedJavaFile result) { - // Match ONLY top-level (public) interface or class declarations - // Top-level types MUST have 'public' modifier in Java - // Use a simpler pattern first, then manually extract type parameters - // Handle modifiers like abstract, final, static (in any order) - def typePattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?public\s+(?:(?:abstract|final|static)\s+)*(interface|class)\s+(\w+)/ - - def matcher = content =~ typePattern - while (matcher.find()) { - JavaType type = new JavaType() - type.jsdoc = matcher.group(1)?.trim() - type.isInterface = matcher.group(2) == 'interface' - type.isClass = matcher.group(2) == 'class' - type.name = matcher.group(3) - - // Manually extract type parameters with balanced bracket matching - int afterName = matcher.end() - String remainder = content.substring(afterName) - - // Check if there are type parameters - if (remainder.trim().startsWith('<')) { - int startIndex = remainder.indexOf('<') - int depth = 0 - int endIndex = -1 - - for (int i = startIndex; i < remainder.length(); i++) { - char c = remainder.charAt(i) - if (c == '<') { - depth++ - } else if (c == '>') { - depth-- - if (depth == 0) { - endIndex = i - break - } - } - } - - if (endIndex > startIndex) { - type.typeParams = remainder.substring(startIndex + 1, endIndex).trim() - // Parse type parameters with full class names - type.parsedTypeParams = parseTypeParams(type.typeParams, result) - remainder = remainder.substring(endIndex + 1) - } - } - - // Now parse extends and implements - def extendsPattern = ~/\s+extends\s+([\w.<>,\s]+?)(?:\s+implements|\s*\{)/ - def extendsMatcher = remainder =~ extendsPattern - if (extendsMatcher.find()) { - type.extendsType = extendsMatcher.group(1)?.trim() - } - - def implementsPattern = ~/\s+implements\s+([\w.<>,\s]+?)\s*\{/ - def implementsMatcher = remainder =~ implementsPattern - if (implementsMatcher.find()) { - type.implementsTypes = implementsMatcher.group(1)?.split(',')?.collect { it.trim() } ?: [] - } else { - type.implementsTypes = [] - } - - // Find the opening brace for the body - int braceIndex = content.indexOf('{', afterName) - if (braceIndex == -1) { - continue // No body found, skip this type - } - - // Find the body of this type - int bodyStart = braceIndex - int bodyEnd = findMatchingBrace(content, bodyStart) - if (bodyEnd > bodyStart) { - String body = content.substring(bodyStart + 1, bodyEnd) - - // Parse methods - pass original body for JSDoc extraction - type.methods = parseMethods(body) - - // Parse nested types - type.nestedTypes = parseNestedTypes(body, type.name) - - // Parse fields (for classes) - if (type.isClass) { - type.fields = parseFields(body) - } - } - - result.types << type - } - } - - /** - * Remove nested type bodies from a string so we only parse top-level methods - */ - private String removeNestedTypeBodies(String body) { - StringBuilder result = new StringBuilder() - int depth = 0 - boolean inNestedType = false - int nestedStart = -1 - - // Find nested type declarations and remove their bodies - def nestedPattern = ~/(?:public\s+)?(?:static\s+)?(interface|class)\s+\w+/ - - int i = 0 - while (i < body.length()) { - char c = body.charAt(i) - - if (c == '{') { - if (!inNestedType) { - // Check if this brace starts a nested type - String before = body.substring(Math.max(0, i - 100), i) - if (before =~ /(?:public\s+)?(?:static\s+)?(?:interface|class)\s+\w+[^{]*$/) { - inNestedType = true - nestedStart = i - depth = 1 - i++ - continue - } - } - if (inNestedType) { - depth++ - } - } else if (c == '}') { - if (inNestedType) { - depth-- - if (depth == 0) { - inNestedType = false - // Don't add the nested type body to result - i++ - continue - } - } - } - - if (!inNestedType) { - result.append(c) - } - i++ - } - - return result.toString() - } - - private List parseMethods(String body) { - List methods = [] - - // Remove nested type bodies first to only get top-level methods - String topLevelBody = removeNestedTypeBodies(body) - - // Match method signatures - handles complex generics - // Anchored with (?m)^ to prevent matching inside // comment lines - // Capture JSDoc in group 1, returnType in group 2, methodName in group 3, params in group 4 - def methodPattern = ~/(?m)^\s*(\/\*\*[\s\S]*?\*\/\s*)?(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+|protected\s+|private\s+)?(?:static\s+)?(?:abstract\s+)?(?:default\s+)?(?:synchronized\s+)?(?:final\s+)?(?:<[^>]+>\s+)?(\w[\w.<>,\[\]\s]*?)\s+(\w+)\s*\(([^)]*)\)\s*(?:throws\s+[\w,\s]+)?[;{]/ - - def matcher = topLevelBody =~ methodPattern - while (matcher.find()) { - String jsdoc = matcher.group(1)?.trim() - String returnType = matcher.group(2).trim() - String methodName = matcher.group(3) - - // Skip constructors - where return type is a visibility modifier - // or the method name matches the class name (which we'd need to track) - if (['public', 'protected', 'private', 'abstract', 'static', 'final', 'synchronized', 'native', 'strictfp'].contains(returnType)) { - continue - } - - JavaMethod method = new JavaMethod() - method.returnType = returnType - method.name = methodName - method.parameters = parseParameters(matcher.group(4)) - method.jsdoc = jsdoc - - methods << method - } - - return methods - } - - private List parseFields(String body) { - List fields = [] - - // Remove nested type bodies first - String topLevelBody = removeNestedTypeBodies(body) - - // Capture JSDoc in group 1, visibility in group 2, fieldType in group 3, fieldName in group 4 - def fieldPattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?(public\s+|protected\s+|private\s+)(?:static\s+)?(?:final\s+)?(\w[\w.<>,\[\]]*)\s+(\w+)\s*[;=]/ - - def matcher = topLevelBody =~ fieldPattern - while (matcher.find()) { - String jsdoc = matcher.group(1)?.trim() - String fieldType = matcher.group(3).trim() - String fieldName = matcher.group(4) - - // Skip Java keywords that might be mismatched - if (['return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'throw', 'try', 'catch', 'finally', 'new', 'this', 'super'].contains(fieldType)) { - continue - } - - JavaField field = new JavaField() - field.type = fieldType - field.name = fieldName - field.jsdoc = jsdoc - - fields << field - } - - return fields - } - - private List parseNestedTypes(String body, String parentName) { - List nestedTypes = [] - - // Capture JSDoc in group 1, interface/class in group 2, name in group 3, typeParams in group 4, extends in group 5 - def nestedPattern = ~/(\/\*\*[\s\S]*?\*\/\s*)?(?:@\w+(?:\([^)]*\))?\s*)*(?:public\s+)?(?:static\s+)?(interface|class)\s+(\w+)(?:<([^>]+)>)?(?:\s+extends\s+([\w.<>,\s]+))?\s*\{/ - - // We need to track position and skip over bodies of found types to avoid finding nested-nested types - int searchStart = 0 - def matcher = nestedPattern.matcher(body) - - while (matcher.find(searchStart)) { - JavaType nested = new JavaType() - nested.jsdoc = matcher.group(1)?.trim() - nested.isInterface = matcher.group(2) == 'interface' - nested.isClass = matcher.group(2) == 'class' - nested.name = matcher.group(3) - nested.typeParams = matcher.group(4) - nested.extendsType = matcher.group(5)?.trim() - - int bodyStart = matcher.end() - 1 - int bodyEnd = findMatchingBrace(body, bodyStart) - if (bodyEnd > bodyStart) { - String nestedBody = body.substring(bodyStart + 1, bodyEnd) - nested.methods = parseMethods(nestedBody) - // Recursively parse nested types within this nested type - nested.nestedTypes = parseNestedTypes(nestedBody, nested.name) - - // Skip past the entire body of this type for the next search - searchStart = bodyEnd + 1 - } else { - // If we couldn't find the matching brace, move past this match - searchStart = matcher.end() - } - - nestedTypes << nested - } - - return nestedTypes - } - - private List parseParameters(String paramsStr) { - List params = [] - if (paramsStr == null || paramsStr.trim().isEmpty()) return params - - // Handle complex generic parameters - List paramParts = splitParameters(paramsStr) - - paramParts.each { part -> - part = part.trim() - if (part.isEmpty()) return - - // Handle varargs - boolean isVarargs = part.contains('...') - part = part.replace('...', '[]') - - // Split type and name - int lastSpace = part.lastIndexOf(' ') - if (lastSpace > 0 && lastSpace < part.length() - 1) { - JavaParameter param = new JavaParameter() - param.type = part.substring(0, lastSpace).trim() - param.name = part.substring(lastSpace + 1).trim() - param.isVarargs = isVarargs - params << param - } else if (lastSpace == -1 && !part.isEmpty()) { - // No space found - might be a single token, skip it - // This can happen with malformed or unusual parameter declarations - } - } - - return params - } - - /** - * Split parameters handling nested generics - */ - private List splitParameters(String params) { - List result = [] - int depth = 0 - StringBuilder current = new StringBuilder() - - params.each { ch -> - if (ch == '<') depth++ - else if (ch == '>') depth-- - else if (ch == ',' && depth == 0) { - result << current.toString() - current = new StringBuilder() - return - } - current.append(ch) - } - - if (current.length() > 0) { - result << current.toString() - } - - return result - } - - private String extractJsDocBefore(String content, int position) { - // Look backwards for JSDoc - String before = content.substring(0, position) - def jsdocMatcher = before =~ /\/\*\*[\s\S]*?\*\/\s*$/ - if (jsdocMatcher.find()) { - return jsdocMatcher.group(0).trim() - } - return null - } - - private int findMatchingBrace(String content, int start) { - int depth = 0 - for (int i = start; i < content.length(); i++) { - char c = content.charAt(i) - if (c == '{') depth++ - else if (c == '}') { - depth-- - if (depth == 0) return i - } - } - return -1 - } - - private String removeBlockComments(String content) { - // Remove JSDoc and block comments for structure parsing - return content.replaceAll(/\/\*[\s\S]*?\*\//, '') - } - - /** - * Generate TypeScript content from parsed Java - */ - String generateTypeScript(ParsedJavaFile parsed, String currentPath) { - StringBuilder sb = new StringBuilder() - - // Header comment - sb.append('/**\n') - sb.append(' * Generated from Java file for CustomNPC+ Minecraft Mod 1.7.10\n') - sb.append(" * Package: ${parsed.packageName}\n") - sb.append(' */\n\n') - - parsed.types.each { type -> - generateType(sb, type, parsed, currentPath, '') - } - - return sb.toString() - } - - private void generateType(StringBuilder sb, JavaType type, ParsedJavaFile parsed, String currentPath, String indent) { - // Build a set of type parameter names for this type (e.g., "T", "U", etc.) - Set typeParamNames = type.parsedTypeParams?.collect { it.name }?.toSet() ?: [] as Set - - // JSDoc - String javaFqn = buildJavaFqn(parsed.packageName, type.name) - String typeJsDoc = ensureJavaFqnTag(type.jsdoc, javaFqn, indent) - if (typeJsDoc) { - sb.append(typeJsDoc) - sb.append('\n') - } - - // Type declaration - String keyword = type.isClass ? 'export class' : 'export interface' - sb.append("${indent}${keyword} ${type.name}") - - // Type parameters with full class name bounds in JSDoc comment - if (type.typeParams) { - sb.append("<${convertTypeParams(type.typeParams, type.parsedTypeParams)}>") - } - - // Extends - skip if extending itself - if (type.extendsType && type.extendsType != type.name) { - sb.append(" extends ${convertType(type.extendsType, parsed, currentPath, typeParamNames)}") - } - - sb.append(' {\n') - - // Methods - compact format, no comments - type.methods.each { method -> - generateMethod(sb, method, parsed, currentPath, indent + ' ', typeParamNames) - } - - // Fields (for classes) - type.fields.each { field -> - generateField(sb, field, parsed, currentPath, indent + ' ', typeParamNames) - } - - sb.append("${indent}}\n") - - // Nested types as namespace - if (!type.nestedTypes.isEmpty()) { - sb.append("\n${indent}export namespace ${type.name} {\n") - type.nestedTypes.each { nested -> - // Always generate as interface, even if empty - // Empty interfaces with extends are important for type hierarchy - generateNestedType(sb, nested, type.name, type.name, parsed, currentPath, indent + ' ') - } - sb.append("${indent}}\n") - } - sb.append('\n') - } - - /** - * Generate a nested type (interface/class within a namespace) - * Recursively handles nested types that themselves have nested types - */ - private void generateNestedType(StringBuilder sb, JavaType type, String parentTypeName, String parentTypeChain, ParsedJavaFile parsed, String currentPath, String indent) { - // Build a set of type parameter names for this nested type - Set typeParamNames = type.parsedTypeParams?.collect { it.name }?.toSet() ?: [] as Set - - // JSDoc - String nestedChain = parentTypeChain ? "${parentTypeChain}.${type.name}" : type.name - String javaFqn = buildJavaFqn(parsed.packageName, nestedChain) - String nestedJsDoc = ensureJavaFqnTag(type.jsdoc, javaFqn, indent) - if (nestedJsDoc) { - sb.append(nestedJsDoc) - sb.append('\n') - } - - // Type declaration - String keyword = type.isClass ? 'export class' : 'export interface' - sb.append("${indent}${keyword} ${type.name}") - - // Type parameters with full class name bounds - if (type.typeParams) { - sb.append("<${convertTypeParams(type.typeParams, type.parsedTypeParams)}>") - } - - // Extends - handle parent type reference specially, skip if extending itself - if (type.extendsType && type.extendsType != type.name) { - String extendsRef = convertTypeForNested(type.extendsType, parentTypeName, parsed, currentPath, typeParamNames) - sb.append(" extends ${extendsRef}") - } - - sb.append(' {\n') - - // Methods - compact format, no comments - type.methods.each { method -> - generateMethod(sb, method, parsed, currentPath, indent + ' ', typeParamNames) - } - - sb.append("${indent}}\n") - - // If this nested type also has nested types, create a namespace for them - if (!type.nestedTypes.isEmpty()) { - sb.append("${indent}export namespace ${type.name} {\n") - type.nestedTypes.each { nested -> - // Always generate as interface, even if empty - // Empty interfaces with extends are important for type hierarchy - generateNestedType(sb, nested, type.name, nestedChain, parsed, currentPath, indent + ' ') - } - sb.append("${indent}}\n") - } - } - - /** - * Convert type reference for nested types - handle parent type specially - */ - private String convertTypeForNested(String javaType, String parentTypeName, ParsedJavaFile parsed, String currentPath, Set typeParamNames) { - if (javaType == null || javaType.isEmpty()) return 'any' - - javaType = javaType.trim() - - // If the type is the parent type name, use it directly (not as import from same file) - if (javaType == parentTypeName) { - return parentTypeName - } - - // Otherwise use normal conversion - return convertType(javaType, parsed, currentPath, typeParamNames) - } - - private void generateMethod(StringBuilder sb, JavaMethod method, ParsedJavaFile parsed, String currentPath, String indent, Set typeParamNames) { - // JSDoc - if (method.jsdoc) { - sb.append(convertJsDoc(method.jsdoc, indent)) - sb.append('\n') - } - - sb.append("${indent}${method.name}(") - - // Parameters - List paramStrs = method.parameters.collect { param -> - String tsType = convertType(param.type, parsed, currentPath, typeParamNames) - if (param.isVarargs) { - return "...${param.name}: ${tsType}" - } - return "${param.name}: ${tsType}" - } - sb.append(paramStrs.join(', ')) - - sb.append('): ') - sb.append(convertType(method.returnType, parsed, currentPath, typeParamNames)) - sb.append(';\n') - } - - private void generateField(StringBuilder sb, JavaField field, ParsedJavaFile parsed, String currentPath, String indent, Set typeParamNames) { - if (field.jsdoc) { - sb.append(convertJsDoc(field.jsdoc, indent)) - sb.append('\n') - } - sb.append("${indent}${field.name}: ${convertType(field.type, parsed, currentPath, typeParamNames)};\n") - } - - /** - * Convert Java type to TypeScript type - * @param typeParamNames Set of type parameter names from the enclosing type (e.g., "T", "U") - */ - String convertType(String javaType, ParsedJavaFile parsed, String currentPath, Set typeParamNames = [] as Set) { - if (javaType == null || javaType.isEmpty()) return 'any' - - javaType = javaType.trim() - - // Check if it's a type parameter (like T, U, etc.) - if (typeParamNames.contains(javaType)) { - return javaType - } - - // Check primitives first - if (PRIMITIVE_MAPPINGS.containsKey(javaType)) { - return PRIMITIVE_MAPPINGS[javaType] - } - - // Handle arrays - if (javaType.endsWith('[]') && javaType.length() > 2) { - String baseType = javaType.substring(0, javaType.length() - 2) - return convertType(baseType, parsed, currentPath, typeParamNames) + '[]' - } - - // Handle generics - if (javaType.contains('<')) { - return convertGenericType(javaType, parsed, currentPath, typeParamNames) - } - - // Check if it is an API type (needs import) - String importPath = resolveImportPath(javaType, parsed, currentPath) - if (importPath != null) { - return "import('${importPath}').${javaType}" - } - - // Check if it is a java.* type - String fullType = resolveFullType(javaType, parsed) - if (fullType != null && fullType.startsWith('java.')) { - return "Java.${fullType}" - } - - // Default - return as is (might be a type parameter like T, or unknown type) - return javaType - } - - private String convertGenericType(String type, ParsedJavaFile parsed, String currentPath, Set typeParamNames = [] as Set) { - int ltIndex = type.indexOf('<') - int gtIndex = type.lastIndexOf('>') - if (ltIndex == -1 || gtIndex == -1 || ltIndex >= gtIndex) { - // Malformed generic, return as-is - return type - } - String baseType = type.substring(0, ltIndex).trim() - String genericPart = type.substring(ltIndex + 1, gtIndex).trim() - - // Handle common collections - switch (baseType) { - case 'List': - case 'ArrayList': - case 'LinkedList': - case 'Collection': - case 'Set': - case 'HashSet': - case 'Queue': - return convertType(genericPart, parsed, currentPath, typeParamNames) + '[]' - - case 'Map': - case 'HashMap': - case 'LinkedHashMap': - List parts = splitGenericParams(genericPart) - if (parts.size() >= 2) { - String keyType = convertType(parts[0], parsed, currentPath, typeParamNames) - String valueType = convertType(parts[1], parsed, currentPath, typeParamNames) - return "Record<${keyType}, ${valueType}>" - } - return 'Record' - - case 'Optional': - return convertType(genericPart, parsed, currentPath, typeParamNames) + ' | null' - - // Functional interfaces - case 'Consumer': - return "(arg: ${convertType(genericPart, parsed, currentPath, typeParamNames)}) => void" - - case 'Supplier': - return "() => ${convertType(genericPart, parsed, currentPath, typeParamNames)}" - - case 'Function': - List funcParts = splitGenericParams(genericPart) - if (funcParts.size() >= 2) { - return "(arg: ${convertType(funcParts[0], parsed, currentPath, typeParamNames)}) => ${convertType(funcParts[1], parsed, currentPath, typeParamNames)}" - } - return '(arg: any) => any' - - case 'Predicate': - return "(arg: ${convertType(genericPart, parsed, currentPath, typeParamNames)}) => boolean" - - case 'BiConsumer': - List biParts = splitGenericParams(genericPart) - if (biParts.size() >= 2) { - return "(arg1: ${convertType(biParts[0], parsed, currentPath, typeParamNames)}, arg2: ${convertType(biParts[1], parsed, currentPath, typeParamNames)}) => void" - } - return '(arg1: any, arg2: any) => void' - - case 'BiFunction': - List biFuncParts = splitGenericParams(genericPart) - if (biFuncParts.size() >= 3) { - return "(arg1: ${convertType(biFuncParts[0], parsed, currentPath, typeParamNames)}, arg2: ${convertType(biFuncParts[1], parsed, currentPath, typeParamNames)}) => ${convertType(biFuncParts[2], parsed, currentPath, typeParamNames)}" - } - return '(arg1: any, arg2: any) => any' - - default: - // Regular generic type - String convertedBase = convertType(baseType, parsed, currentPath, typeParamNames) - List convertedParams = splitGenericParams(genericPart).collect { - convertType(it, parsed, currentPath, typeParamNames) - } - // For import types, we cannot add generics easily, so simplify - if (convertedBase.startsWith('import(')) { - return convertedBase - } - return "${convertedBase}<${convertedParams.join(', ')}>" - } - } - - private List splitGenericParams(String params) { - List result = [] - int depth = 0 - StringBuilder current = new StringBuilder() - - params.each { ch -> - if (ch == '<') depth++ - else if (ch == '>') depth-- - else if (ch == ',' && depth == 0) { - result << current.toString().trim() - current = new StringBuilder() - return - } - current.append(ch) - } - - if (current.length() > 0) { - result << current.toString().trim() - } - - return result - } - - /** - * Convert Java type parameters to TypeScript - * Outputs format like: T extends EntityPlayerMP /`*` net.minecraft.entity.player.EntityPlayerMP `*`/ - * This allows runtime parsing to extract the full Java class name for the type bound. - */ - private String convertTypeParams(String typeParams, List parsedParams) { - if (typeParams == null || typeParams.isEmpty()) return typeParams - if (parsedParams == null || parsedParams.isEmpty()) return typeParams - - // Build result by iterating over parsed params - List convertedParams = [] - for (TypeParamInfo info in parsedParams) { - StringBuilder sb = new StringBuilder() - sb.append(info.name) - - if (info.boundType != null) { - sb.append(" extends ") - sb.append(info.boundType) - - // Add full class name as JSDoc-style comment if available - if (info.fullBoundType != null) { - sb.append(" /* ${info.fullBoundType} */") - } - } - - convertedParams << sb.toString() - } - - return convertedParams.join(', ') - } - - private String resolveImportPath(String typeName, ParsedJavaFile parsed, String currentPath) { - // Check imports for this type - String fullType = resolveFullType(typeName, parsed) - - if (fullType == null) return null - - // Check if it is an API type - int lastDotIndex = fullType.lastIndexOf('.') - if (lastDotIndex == -1) return null - String packagePrefix = fullType.substring(0, lastDotIndex) - if (apiPackages.any { fullType.startsWith(it) }) { - // Calculate relative path - String typeFilePath = fullType.replace('.', '/') + '.d.ts' - return calculateRelativePath(currentPath, typeFilePath) - } - - return null - } - - private String resolveFullType(String typeName, ParsedJavaFile parsed) { - // Check explicit imports - String explicitImport = parsed.imports.find { it.endsWith(".${typeName}") } - if (explicitImport) return explicitImport - - // Check wildcard imports - parsed.imports.each { imp -> - if (imp.endsWith('.*')) { - // Would need classpath to resolve, skip for now - } - } - - // Same package - return "${parsed.packageName}.${typeName}" - } - - /** - * Parse type parameters like "T extends EntityPlayerMP" into structured TypeParamInfo - * with full class names resolved from imports - */ - private List parseTypeParams(String typeParams, ParsedJavaFile parsed) { - List result = [] - if (typeParams == null || typeParams.isEmpty()) return result - - // Split by comma, but respect nested angle brackets - List params = splitGenericParams(typeParams) - - for (String param in params) { - param = param.trim() - TypeParamInfo info = new TypeParamInfo() - - // Check for "T extends BoundType" pattern - def extendsMatcher = param =~ /(\w+)\s+extends\s+(.+)/ - if (extendsMatcher.find()) { - info.name = extendsMatcher.group(1).trim() - String boundType = extendsMatcher.group(2).trim() - - // Handle generic bounds like "Comparable" - extract base type - int angleIndex = boundType.indexOf('<') - String baseBoundType = angleIndex > 0 ? boundType.substring(0, angleIndex) : boundType - - info.boundType = baseBoundType - info.fullBoundType = resolveFullType(baseBoundType, parsed) - } else { - // Just a type param like "T" with no explicit bound - info.name = param - info.boundType = null - info.fullBoundType = null - } - - result << info - } - - return result - } - - private String calculateRelativePath(String fromPath, String toPath) { - // Calculate relative path between two .d.ts files - String[] fromParts = fromPath.split('/') - String[] toParts = toPath.split('/') - - // Find common prefix - int common = 0 - while (common < fromParts.length - 1 && common < toParts.length && fromParts[common] == toParts[common]) { - common++ - } - - // Build relative path - StringBuilder result = new StringBuilder() - - // Go up from current location - int ups = fromParts.length - common - 1 - if (ups == 0) { - result.append('./') - } else { - for (int i = 0; i < ups; i++) { - result.append('../') - } - } - - // Go down to target - for (int i = common; i < toParts.length; i++) { - if (i > common) result.append('/') - result.append(toParts[i]) - } - - // Remove .d.ts extension for imports - String path = result.toString() - if (path.endsWith('.d.ts')) { - path = path.substring(0, path.length() - 5) - } - - return path - } - - /** - * Convert JavaDoc to JSDoc format - */ - private String convertJsDoc(String jsdoc, String indent) { - if (jsdoc == null) return '' - - // Already in JSDoc format, just fix indentation - String[] lines = jsdoc.split('\n') - return lines.collect { line -> - String trimmed = line.trim() - if (trimmed.startsWith('*')) { - return "${indent} ${trimmed}" - } else if (trimmed.startsWith('/**') || trimmed.startsWith('*/')) { - return "${indent}${trimmed}" - } else { - return "${indent}${trimmed}" - } - }.join('\n') - } - - private String ensureJavaFqnTag(String jsdoc, String javaFqn, String indent) { - if (javaFqn == null || javaFqn.isEmpty()) { - return convertJsDoc(jsdoc, indent) - } - - if (jsdoc != null && jsdoc.contains('@javaFqn')) { - return convertJsDoc(jsdoc, indent) - } - - if (jsdoc == null || jsdoc.trim().isEmpty()) { - return "${indent}/**\n${indent} * @javaFqn ${javaFqn}\n${indent} */" - } - - String converted = convertJsDoc(jsdoc, indent) - int endIndex = converted.lastIndexOf('*/') - if (endIndex >= 0) { - String insert = "${indent} * @javaFqn ${javaFqn}\n" - return converted.substring(0, endIndex) + insert + converted.substring(endIndex) - } - - return converted + "\n${indent} * @javaFqn ${javaFqn}" - } - - private String buildJavaFqn(String packageName, String typeName) { - if (typeName == null || typeName.isEmpty()) return null - if (packageName == null || packageName.isEmpty()) return typeName - return packageName + '.' + typeName - } - - /** - * 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') || - isEventPackage - ) - - if (isEventType && !type.nestedTypes.isEmpty()) { - 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) - } - } - } - } - - private String deriveHookName(String eventName) { - // Convert event names to hook function names - // e.g., "InitEvent" -> "init", "DamagedEvent" -> "damaged" - String name = eventName - if (name.endsWith('Event') && name.length() > 5) { - name = name.substring(0, name.length() - 5) - } - // Convert to camelCase - if (name.isEmpty()) return 'event' - String hookName = name.length() > 1 ? (name.substring(0, 1).toLowerCase() + name.substring(1)) : name.toLowerCase() - - // Handle JavaScript reserved words - Set reservedWords = ['break', 'case', 'catch', 'continue', 'debugger', 'default', 'delete', - 'do', 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof', - 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var', - 'void', 'while', 'with', 'class', 'const', 'enum', 'export', 'extends', - 'import', 'super', 'implements', 'interface', 'let', 'package', 'private', - 'protected', 'public', 'static', 'yield'] as Set - - if (reservedWords.contains(hookName)) { - // Prefix with 'on' for reserved words - hookName = 'on' + name - } - - return hookName - } - - /** - * Generate index.d.ts with all type aliases - */ - private void generateIndexFile() { - StringBuilder sb = new StringBuilder() - - sb.append('/**\n') - sb.append(' * Centralized global declarations for CustomNPC+ scripting.\n') - sb.append(' * Auto-generated - do not edit manually.\n') - sb.append(' */\n\n') - - sb.append('declare global {\n') - sb.append(' // ============================================================================\n') - sb.append(' // TYPE ALIASES - Make all interfaces available globally\n') - sb.append(' // ============================================================================\n\n') - - generatedTypes.sort { a, b -> a.name <=> b.name } - - generatedTypes.each { type -> - if (!type.name.contains('.')) { // Skip nested types here - sb.append(" type ${type.name} = import('./${type.filePath.replace('.d.ts', '')}').${type.name};\n") - } - } - - // Collect all parent types that have nested types (for namespace declarations) - Map> parentToNested = [:] - generatedTypes.each { type -> - if (type.parentType) { - if (!parentToNested.containsKey(type.parentType)) { - parentToNested[type.parentType] = [] - } - parentToNested[type.parentType] << type - } - } - - // Generate namespace declarations for types with nested interfaces - if (!parentToNested.isEmpty()) { - sb.append('\n // ============================================================================\n') - sb.append(' // NESTED INTERFACES - Allow autocomplete like INpcEvent.InitEvent\n') - sb.append(' // ============================================================================\n\n') - - parentToNested.sort { a, b -> a.key <=> b.key }.each { parentName, nestedTypes -> - sb.append(" namespace ${parentName} {\n") - nestedTypes.sort { a, b -> a.name <=> b.name }.each { nested -> - // Extract just the nested type name (e.g., "InitEvent" from "IPlayerEvent.InitEvent") - String nestedName = nested.name.contains('.') ? - nested.name.substring(nested.name.lastIndexOf('.') + 1) : nested.name - sb.append(" interface ${nestedName} extends ${parentName} {}\n") - } - sb.append(" }\n\n") - } - } - - sb.append('}\n\n') - sb.append('export {};\n') - - new File(outputDir, 'index.d.ts').text = sb.toString() - } - - /** - * 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 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") - - // Group hooks by their parent event type (contextNamespace = type.name) - Map>> hooksByEventType = [:].withDefault { [:].withDefault { [] } } - - hooks.each { hookName, hookInfos -> - hookInfos.each { info -> - hooksByEventType[info.contextNamespace][hookName] << info - } - } - - // 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() - } - - // Data classes - static class ParsedJavaFile { - String packageName = '' - List imports = [] - List types = [] - } - - static class JavaType { - String name - String typeParams // Raw string like "T extends EntityPlayerMP" - List parsedTypeParams = [] // Parsed type parameters with full class names - String extendsType - List implementsTypes = [] - boolean isInterface - boolean isClass - String jsdoc - List methods = [] - List fields = [] - List nestedTypes = [] - } - - static class JavaMethod { - String name - String returnType - List parameters = [] - String jsdoc - } - - static class JavaParameter { - String name - String type - boolean isVarargs - } - - static class JavaField { - String name - String type - String jsdoc - } - - static class TypeParamInfo { - String name // e.g., "T" - String boundType // e.g., "EntityPlayerMP" - String fullBoundType // e.g., "net.minecraft.entity.player.EntityPlayerMP" - } - - static class TypeInfo { - String name - String packageName - String filePath - boolean isClass - boolean isInterface - String extendsType - String parentType - } - - static class HookInfo { - 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/gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy b/gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy deleted file mode 100644 index 3165e3744..000000000 --- a/gradle-plugins/src/main/groovy/dts/TypeScriptGeneratorPlugin.groovy +++ /dev/null @@ -1,17 +0,0 @@ -package dts - -import org.gradle.api.Plugin -import org.gradle.api.Project - -/** - * Gradle plugin for generating TypeScript definition files from Java API sources. - */ -class TypeScriptGeneratorPlugin implements Plugin { - - @Override - void apply(Project project) { - if (project.tasks.findByName('generateTypeScriptDefinitions') == null) { - project.tasks.register('generateTypeScriptDefinitions', GenerateTypeScriptTask) - } - } -} \ No newline at end of file diff --git a/gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties b/gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties deleted file mode 100644 index f27cbecea..000000000 --- a/gradle-plugins/src/main/resources/META-INF/gradle-plugins/dts.typescript-generator.properties +++ /dev/null @@ -1 +0,0 @@ -implementation-class=dts.TypeScriptGeneratorPlugin From e2b35d6d549771a93095312794707f5e00a1aa2e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 18 Feb 2026 05:21:42 +0200 Subject: [PATCH 335/337] Method reference and SAM type resolution - add full parser/type-resolver support for Java method and constructor references - infer SAM callback types for named JS script methods with conflict reporting - surface argument-level SAM/type mismatch errors in method validation --- .../script/interpreter/ScriptDocument.java | 459 +++++++++++++++++- .../expression/ExpressionParser.java | 17 +- .../expression/ExpressionTypeResolver.java | 452 +++++++++++++++-- .../interpreter/method/MethodCallInfo.java | 7 + .../script/interpreter/method/MethodInfo.java | 13 +- .../script/interpreter/type/TypeChecker.java | 7 +- 6 files changed, 897 insertions(+), 58 deletions(-) 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 e25a0ccdd..54c7a6d37 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 @@ -121,6 +121,14 @@ public class ScriptDocument { // Excluded regions (strings/comments) - positions where other patterns shouldn't match private final List excludedRanges = new ArrayList<>(); + + // SAM context tracking for named function references + // Maps method name -> first SAM signature applied (for conflict detection) + private final Map scriptMethodSamContexts = new HashMap<>(); + + // Thread-local to communicate SAM conflict errors from resolveIdentifier back to parseMethodArguments + // Set when injectSamParameterTypes detects a conflict, cleared after argument is created + private static final ThreadLocal CURRENT_SAM_CONFLICT_ERROR = new ThreadLocal<>(); // Type resolver private final TypeResolver typeResolver; @@ -338,6 +346,7 @@ public void formatCodeText() { methodCalls.clear(); externalFieldAssignments.clear(); declarationErrors.clear(); + scriptMethodSamContexts.clear(); // Unified pipeline for both languages List marks = formatUnified(); @@ -942,7 +951,7 @@ private void parseMethodDeclarations() { int paramStart = paramOffset + paramList.indexOf(pn); // Check if JSDoc has a @param tag for this parameter - TypeInfo paramType = TypeInfo.fromClass(Object.class); + TypeInfo paramType = TypeInfo.unresolved("any", "any"); if (jsDoc != null) { JSDocParamTag paramTag = jsDoc.getParamTag(pn); if (paramTag != null && paramTag.getTypeInfo() != null) { @@ -2574,6 +2583,8 @@ private List buildMarks() { if (!isJavaScript()) { markCastTypes(marks); markUnusedImports(marks); + // Mark method reference expressions (target::methodName) + markMethodReferences(marks); } // Mark lambda and method reference operators (-> and ::) @@ -2608,9 +2619,230 @@ private void markLambdaOperators(List marks) { for (int i = 0; i < text.length() - 1; i++) { if (text.charAt(i) == ':' && text.charAt(i + 1) == ':') { if (!isExcluded(i)) { - marks.add(new ScriptLine.Mark(i, i + 2, TokenType.KEYWORD)); + marks.add(new ScriptLine.Mark(i, i + 2, TokenType.DEFAULT)); + } + } + } + } + + /** + * Pattern for method reference: target::methodName + * target can be: identifier, qualified name (a.b.c), or 'this'/'super' + */ + private static final java.util.regex.Pattern METHOD_REF_PATTERN = + java.util.regex.Pattern.compile("([a-zA-Z_$][a-zA-Z0-9_$]*(?:\\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\\s*::\\s*([a-zA-Z_$][a-zA-Z0-9_$]*|new)"); + + /** + * Mark method reference expressions with appropriate token types. + * Target gets its resolved type's token, method name gets METHOD_CALL if valid. + * Validates that the method exists on the target type. + */ + private void markMethodReferences(List marks) { + java.util.regex.Matcher m = METHOD_REF_PATTERN.matcher(text); + while (m.find()) { + int targetStart = m.start(1); + int targetEnd = m.end(1); + int methodStart = m.start(2); + int methodEnd = m.end(2); + + // Skip if in excluded region (string/comment) + if (isExcluded(targetStart) || isExcluded(methodStart)) { + continue; + } + + String target = m.group(1); + String methodName = m.group(2); + + // Check if there are parentheses after the method name (invalid for method references) + if (methodEnd < text.length() && text.charAt(methodEnd) == '(') { + // Mark the :: as error + int doubleColonPos = targetEnd; + while (doubleColonPos < methodStart && text.charAt(doubleColonPos) != ':') { + doubleColonPos++; + } + if (doubleColonPos < methodStart) { + marks.add(new ScriptLine.Mark(doubleColonPos, doubleColonPos + 2, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Method references cannot have parentheses. Use '" + target + "::" + methodName + "' instead of '" + target + "::" + methodName + "()'"))); + } + // Also mark the method name as error + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Method references cannot have parentheses"))); + // Mark opening paren as error too + marks.add(new ScriptLine.Mark(methodEnd, methodEnd + 1, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Remove parentheses from method reference"))); + continue; // Skip normal processing for this invalid reference + } + + // Mark the target based on what it resolves to + markMethodRefTarget(marks, target, targetStart, targetEnd); + + // Resolve the target type to validate method existence + TypeInfo targetType = resolveMethodRefTargetType(target, targetStart); + + if (targetType != null && targetType.isResolved()) { + // Handle constructor references (::new) + if ("new".equals(methodName)) { + if (targetType.hasConstructors()) { + MethodInfo ctorInfo = targetType.findConstructor(0); + if (ctorInfo == null) { + List ctors = targetType.getConstructors(); + if (!ctors.isEmpty()) { + ctorInfo = ctors.get(0); + } + } + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.METHOD_CALL, ctorInfo)); + } else { + // No constructors found + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("No constructor found for '" + targetType.getSimpleName() + "'"))); + } + } else if (targetType.hasMethod(methodName)) { + // Method exists - get the MethodInfo for metadata + MethodInfo methodInfo = targetType.getMethodInfo(methodName); + if (methodInfo == null) { + // Try getting from overloads if single getMethodInfo fails + List overloads = targetType.getAllMethodOverloads(methodName); + if (!overloads.isEmpty()) { + methodInfo = overloads.get(0); + } + } + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.METHOD_CALL, methodInfo)); + } else { + // Method does not exist on target type + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR, + TokenErrorMessage.from("Method '" + methodName + "' not found in '" + targetType.getSimpleName() + "'"))); } + } else { + // Could not resolve target type - mark method as potentially valid (no error) + // This allows for cases where the target type cannot be resolved but might be valid at runtime + marks.add(new ScriptLine.Mark(methodStart, methodEnd, TokenType.UNDEFINED_VAR)); + } + } + } + + /** + * Resolve the target type for a method reference expression. + * @param target The target expression (e.g., "this", "String", "java.util.Arrays") + * @param position The position in the text for scope resolution + * @return The resolved TypeInfo, or null if it cannot be resolved + */ + private TypeInfo resolveMethodRefTargetType(String target, int position) { + // Handle keywords + if ("this".equals(target)) { + // Resolve 'this' to the enclosing type + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null) { + return enclosingType; + } + // For hook methods, resolve to the implied 'this' type + MethodInfo containingMethod = findContainingMethod(position); + if (containingMethod != null && containingMethod.getContainingType() != null) { + return containingMethod.getContainingType(); + } + return null; + } + + if ("super".equals(target)) { + // Resolve 'super' to the parent type of the enclosing class + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasSuperClass()) { + return enclosingType.getSuperClass(); + } + return null; + } + + // Handle qualified names (a.b.ClassName) + if (target.contains(".")) { + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + return typeInfo; + } + // Could not resolve as type - leave unresolved + return null; + } + + // Simple identifier - try as variable first (instance reference like myList::add) + FieldInfo varInfo = resolveVariable(target, position); + if (varInfo != null && varInfo.getTypeInfo() != null) { + return varInfo.getTypeInfo(); + } + + // Try as type name (class reference like String::valueOf) + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + return typeInfo; + } + + return null; + } + + /** + * Mark the target of a method reference with the appropriate token type. + */ + private void markMethodRefTarget(List marks, String target, int start, int end) { + // Handle keywords + if ("this".equals(target)) { + marks.add(new ScriptLine.Mark(start, end, TokenType.KEYWORD)); + return; + } + if ("super".equals(target)) { + marks.add(new ScriptLine.Mark(start, end, TokenType.KEYWORD)); + return; + } + + // Handle qualified names (a.b.ClassName) - mark the whole thing + if (target.contains(".")) { + // Try to resolve as a type + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + marks.add(new ScriptLine.Mark(start, end, TokenType.IMPORTED_CLASS, typeInfo)); + return; + } + // Otherwise mark the parts separately + markQualifiedTargetParts(marks, target, start); + return; + } + + // Simple identifier - determine its token type + // Check for local variable + FieldInfo varInfo = resolveVariable(target, start); + if (varInfo != null) { + TokenType tokenType = varInfo.isParameter() ? TokenType.PARAMETER + : varInfo.isGlobal() ? TokenType.GLOBAL_FIELD + : TokenType.LOCAL_FIELD; + marks.add(new ScriptLine.Mark(start, end, tokenType, varInfo)); + return; + } + + // Check for type name (imported class) + TypeInfo typeInfo = resolveType(target); + if (typeInfo != null && typeInfo.isResolved()) { + marks.add(new ScriptLine.Mark(start, end, TokenType.IMPORTED_CLASS, typeInfo)); + return; + } + + // Unknown - leave as default + } + + /** + * Mark parts of a qualified name like java.util.Arrays::asList + */ + private void markQualifiedTargetParts(List marks, String qualifiedName, int baseStart) { + String[] parts = qualifiedName.split("\\."); + int offset = baseStart; + for (int i = 0; i < parts.length; i++) { + String part = parts[i]; + int partEnd = offset + part.length(); + + if (i == parts.length - 1) { + // Last part is typically the class name + marks.add(new ScriptLine.Mark(offset, partEnd, TokenType.IMPORTED_CLASS)); + } else { + // Package parts + marks.add(new ScriptLine.Mark(offset, partEnd, TokenType.TYPE_DECL)); } + + offset = partEnd + 1; // +1 for the dot } } @@ -3738,8 +3970,9 @@ private void markMethodCalls(List marks) { } if (hasMethod) { - + TypeInfo[] argTypes = arguments.stream().map(MethodCallInfo.Argument::getResolvedType).toArray(TypeInfo[]::new); + // Get best method overload based on argument types resolvedMethod = receiverType.getBestMethodOverload(methodName, argTypes); @@ -3995,14 +4228,22 @@ public List parseMethodArguments(int start, int end, Me TypeInfo expectedParamType = null; if (methodInfo != null && args.size() < methodInfo.getParameters().size()) { FieldInfo parameter = methodInfo.getParameters().get(args.size()); - expectedParamType = parameter.getDeclaredType(); + expectedParamType = parameter.getTypeInfo(); } TypeInfo argType = resolveArgumentType(argText, actualStart, expectedParamType); - args.add(new MethodCallInfo.Argument( - argText, actualStart, actualEnd, argType, true, null - )); + String samConflictError = CURRENT_SAM_CONFLICT_ERROR.get(); + if (samConflictError != null) { + CURRENT_SAM_CONFLICT_ERROR.remove(); + args.add(new MethodCallInfo.Argument( + argText, actualStart, actualEnd, argType, false, samConflictError + )); + } else { + args.add(new MethodCallInfo.Argument( + argText, actualStart, actualEnd, argType, true, null + )); + } } argStart = i + 1; continue; @@ -4141,9 +4382,9 @@ public TypeInfo resolveExpressionType(String expr, int position) { // Check if expression contains operators - if so, use the full expression resolver // Handle cast expressions: (Type)expr, ((Type)expr).method(), etc. // Also route JS function expressions and arrow lambdas through the parser - if (containsOperators(expr) || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) { - return resolveExpressionWithParserAPI(expr, position); - } + if (containsOperators(expr) || expr.contains("::") || expr.contains("->") || expr.startsWith("(") || looksLikeFunctionOrLambda(expr)) { + return resolveExpressionWithParserAPI(expr, position); + } // Invalid expressions starting with brackets if (expr.startsWith("[") || expr.startsWith("]")) { @@ -4270,6 +4511,17 @@ public TypeInfo resolveExpressionType(String expr, int position) { FieldInfo varInfo = resolveVariable(first.name, position); if (varInfo != null) { currentType = varInfo.getTypeInfo(); + } else if (isScriptMethod(first.name)) { + // Script method reference used as an expression. + // In JS, this is commonly used as a SAM callback (e.g., schedule("id", actionFunction)). + TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE; + TypeInfo samType = resolveScriptMethodAsSam(first.name, expectedType); + if (samType != null) { + currentType = samType; + } else if (isJavaScript()) { + // Provide a non-null placeholder type so overload selection can prefer functional-interface params. + currentType = TypeInfo.unresolved("", "__script_method_ref__"); + } } } else { // First segment is a method call - check script methods @@ -4807,10 +5059,17 @@ private ExpressionNode.TypeResolverContext createExpressionResolverContext(int p return new ExpressionNode.TypeResolverContext() { @Override public TypeInfo resolveIdentifier(String name) { - // Check for special keywords first if ("this".equals(name)) { return findEnclosingScriptType(position); } + if ("super".equals(name)) { + // Resolve super to parent class type + ScriptTypeInfo enclosingType = findEnclosingScriptType(position); + if (enclosingType != null && enclosingType.hasSuperClass()) { + return enclosingType.getSuperClass(); + } + return null; + } if ("true".equals(name) || "false".equals(name)) { return TypeInfo.fromPrimitive("boolean"); } @@ -4818,13 +5077,12 @@ public TypeInfo resolveIdentifier(String name) { return TypeInfo.unresolved("null", ""); } - // Try to resolve as a variable + // Variable shadowing: variables take precedence over script methods FieldInfo varInfo = resolveVariable(name, position); if (varInfo != null) { return varInfo.getTypeInfo(); } - // Try as a class name (for static access) if (name.length() > 0) { TypeInfo typeCheck = resolveType(name); if (typeCheck != null && typeCheck.isResolved()) { @@ -4832,6 +5090,13 @@ public TypeInfo resolveIdentifier(String name) { } } + // Named script function as SAM callback: schedule("id", actionFunction) + TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE; + TypeInfo samType = resolveScriptMethodAsSam(name, expectedType); + if (samType != null) { + return samType; + } + return null; } @@ -5268,6 +5533,34 @@ private boolean isScriptMethod(String methodName) { return false; } + private TypeInfo resolveScriptMethodAsSam(String methodName, TypeInfo expectedType) { + if (methodName == null || expectedType == null || !expectedType.isFunctionalInterface() || !isScriptMethod(methodName)) { + return null; + } + + // Java mode: bare method names are invalid as SAM callbacks + if (!isJavaScript()) { + CURRENT_SAM_CONFLICT_ERROR.set( + "Bare method name '" + methodName + "' cannot be used as callback in Java. Use this::" + methodName + ); + return expectedType; + } + + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam == null) { + return null; + } + + int samArity = sam.getParameters().size(); + MethodInfo bestMatch = getScriptMethodBySamArity(methodName, samArity); + if (bestMatch == null) { + return null; + } + + injectSamParameterTypes(bestMatch, sam); + return expectedType; + } + /** * Get the MethodInfo for a script-defined method by name. */ @@ -5353,6 +5646,137 @@ private MethodInfo getScriptMethodInfo(String methodName, TypeInfo[] argTypes) { return candidates.get(0); } + /** + * Get the best matching script method for use as a SAM callback by arity. + * Returns null if no match found, or if multiple overloads match by arity (ambiguous). + * + * @param methodName Name of the script method + * @param samArity Number of parameters in the SAM interface method + * @return The matching MethodInfo, or null if no unambiguous match + */ + private MethodInfo getScriptMethodBySamArity(String methodName, int samArity) { + List matchingByArity = new ArrayList<>(); + + for (MethodInfo method : methods) { + if (method.getName().equals(methodName)) { + if (method.getParameters().size() == samArity) { + matchingByArity.add(method); + } + } + } + + if (matchingByArity.size() == 1) { + return matchingByArity.get(0); + } + + if (matchingByArity.isEmpty()) { + return getScriptMethodInfo(methodName); + } + + CURRENT_SAM_CONFLICT_ERROR.set( + "Ambiguous overload for '" + methodName + "' with " + samArity + " parameter(s): " + + matchingByArity.size() + " overloads match" + ); + return null; + } + + private void injectSamParameterTypes(MethodInfo scriptMethod, MethodInfo sam) { + String methodName = scriptMethod.getName(); + + MethodInfo previousSam = scriptMethodSamContexts.get(methodName); + if (previousSam != null) { + if (!areSamSignaturesCompatible(previousSam, sam)) { + String existingSig = formatSamSignature(previousSam); + String newSig = formatSamSignature(sam); + CURRENT_SAM_CONFLICT_ERROR.set( + "Function '" + methodName + "' used in incompatible SAM contexts: " + + existingSig + " vs " + newSig + ); + return; + } + } else { + scriptMethodSamContexts.put(methodName, sam); + } + + List scriptParams = scriptMethod.getParameters(); + List samParams = sam.getParameters(); + + if (scriptParams.size() != samParams.size()) { + return; + } + + for (int i = 0; i < scriptParams.size(); i++) { + FieldInfo scriptParam = scriptParams.get(i); + TypeInfo samParamType = samParams.get(i).getTypeInfo(); + + if (samParamType == null) { + continue; + } + + TypeInfo declaredType = scriptParam.getDeclaredType(); + + boolean hasExplicitType = declaredType != null + && declaredType.isResolved() + && !"any".equals(declaredType.getFullName()); + + if (hasExplicitType) { + if (!TypeChecker.isTypeCompatible(declaredType, samParamType)) { + scriptMethod.addSamTypeError(i, samParamType, declaredType); + } + } else { + scriptParam.setInferredType(samParamType); + } + } + } + + private boolean areSamSignaturesCompatible(MethodInfo sam1, MethodInfo sam2) { + List params1 = sam1.getParameters(); + List params2 = sam2.getParameters(); + + if (params1.size() != params2.size()) { + return false; + } + + for (int i = 0; i < params1.size(); i++) { + TypeInfo type1 = params1.get(i).getTypeInfo(); + TypeInfo type2 = params2.get(i).getTypeInfo(); + + if (type1 == null || type2 == null) { + continue; + } + + if (!TypeChecker.isTypeCompatible(type1, type2) && !TypeChecker.isTypeCompatible(type2, type1)) { + return false; + } + } + + TypeInfo return1 = sam1.getReturnType(); + TypeInfo return2 = sam2.getReturnType(); + + if (return1 != null && return2 != null) { + if (!TypeChecker.isTypeCompatible(return1, return2) && !TypeChecker.isTypeCompatible(return2, return1)) { + return false; + } + } + + return true; + } + + private String formatSamSignature(MethodInfo sam) { + StringBuilder sb = new StringBuilder(); + sb.append("("); + List params = sam.getParameters(); + for (int i = 0; i < params.size(); i++) { + if (i > 0) sb.append(", "); + TypeInfo type = params.get(i).getTypeInfo(); + sb.append(type != null ? type.getSimpleName() : "?"); + } + sb.append(") -> "); + TypeInfo returnType = sam.getReturnType(); + sb.append(returnType != null ? returnType.getSimpleName() : "void"); + return sb.toString(); + } + /** * Check if a method belongs to a script-defined type (class/interface/enum). * Returns true if the method is defined inside a script type. @@ -5549,6 +5973,15 @@ private void markVariables(List marks) { if (isUppercase) continue; + // Check if it's a script method used as a value (e.g., in schedule("action", methodName)) + if (isScriptMethod(name)) { + MethodInfo scriptMethod = getScriptMethodInfo(name); + if (scriptMethod != null) { + marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.METHOD_CALL, scriptMethod)); + continue; + } + } + // Unknown variable - mark as undefined if (containingMethod != null) { marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR, callInfo)); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java index dacd60618..cc61f1f5a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionParser.java @@ -462,19 +462,10 @@ private ExpressionNode parseAccessChain(ExpressionNode base) { int end = current().getEnd(); advance(); - // Determine if this is a static or instance method reference - boolean isStatic = false; - if (base instanceof ExpressionNode.IdentifierNode) { - // Could be Class::method or obj::method - // We'll determine this during type resolution - String targetName = ((ExpressionNode.IdentifierNode) base).getName(); - // If targetName starts with uppercase, likely a class - if (targetName.length() > 0) { - isStatic = Character.isUpperCase(targetName.charAt(0)); - } - } - - base = new ExpressionNode.MethodReferenceNode(base, methodName, isStatic, base.getStart(), end); + // isStatic is determined during type resolution, not parsing + // We pass false as a placeholder - ExpressionTypeResolver will compute + // the real value based on whether target resolves to a ClassTypeInfo + base = new ExpressionNode.MethodReferenceNode(base, methodName, false, base.getStart(), end); // Method references don't chain further (can't do obj::method.something) break; } else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java index 70c744199..b3944e9e7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/expression/ExpressionTypeResolver.java @@ -4,7 +4,12 @@ import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope; import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo; import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.ClassTypeInfo; +import noppes.npcs.client.gui.util.script.interpreter.type.OverloadSelector; +import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker; import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo; + +import java.util.ArrayList; import java.util.List; public class ExpressionTypeResolver { @@ -372,49 +377,436 @@ private InnerCallableScope findJSFunctionScopeByPosition(int expressionRelativeP return null; } + // ==================== Method Reference Resolution ==================== + + /** + * Resolve the type of a method reference expression (target::methodName). + * + * Supports all Java method reference forms: + * - this::method - Instance method on current object + * - super::method - Instance method on superclass + * - variable::method - Instance method on a variable's value + * - ClassName::method - Static method OR unbound instance method + * - pkg.ClassName::method - Fully qualified class reference + * - ClassName::new - Constructor reference + * - Type[]::new - Array constructor reference + * + * @param methodRef The method reference AST node + * @return The resolved type (functional interface type if valid, Object if invalid) + */ private TypeInfo resolveMethodReferenceType(ExpressionNode.MethodReferenceNode methodRef) { - // Method reference type is determined by the expected functional interface TypeInfo expectedType = CURRENT_EXPECTED_TYPE; - if (expectedType == null) { - // No expected functional interface context - return TypeInfo.fromClass(Object.class); // Fallback + // Must have expected functional interface context + if (expectedType == null || !expectedType.isFunctionalInterface()) { + return TypeInfo.fromClass(Object.class); + } + + MethodInfo sam = expectedType.getSingleAbstractMethod(); + if (sam == null) { + return TypeInfo.fromClass(Object.class); + } + + // Resolve target and determine if it's a class reference (static context) + MethodReferenceTarget resolvedTarget = resolveMethodReferenceTarget(methodRef); + if (resolvedTarget == null || resolvedTarget.type == null) { + // Unresolved target - return expected type to allow forward references + return expectedType; + } + + String methodName = methodRef.getMethodName(); + + // Handle constructor references (ClassName::new or Type[]::new) + if ("new".equals(methodName)) { + return resolveConstructorReference(resolvedTarget, sam, expectedType); + } + + // Handle method references + return resolveMethodReference(resolvedTarget, methodName, sam, expectedType); + } + + /** + * Encapsulates the resolved target of a method reference. + */ + private static class MethodReferenceTarget { + final TypeInfo type; // The resolved type + final boolean isClassRef; // True if target is a class reference (for static access) + + MethodReferenceTarget(TypeInfo type, boolean isClassRef) { + this.type = type; + this.isClassRef = isClassRef; + } + } + + /** + * Resolve the target (left side of ::) for a method reference. + * Returns both the type and whether it's a class reference. + */ + private MethodReferenceTarget resolveMethodReferenceTarget(ExpressionNode.MethodReferenceNode methodRef) { + ExpressionNode target = methodRef.getTarget(); + if (target == null) { + return null; + } + + // Handle simple identifiers: this, super, variable, ClassName + if (target instanceof ExpressionNode.IdentifierNode) { + String name = ((ExpressionNode.IdentifierNode) target).getName(); + return resolveSimpleTarget(name); + } + + // Handle qualified names: pkg.ClassName or outer.Inner + if (target instanceof ExpressionNode.MemberAccessNode) { + return resolveQualifiedTarget((ExpressionNode.MemberAccessNode) target); + } + + // Handle array type targets: Type[] + if (target instanceof ExpressionNode.ArrayAccessNode) { + TypeInfo arrayType = resolveNodeType(target); + if (arrayType != null && arrayType.isResolved()) { + // Array type is always a class reference (for Type[]::new) + return new MethodReferenceTarget(arrayType, true); + } + } + + // Fallback: resolve as expression (e.g., (expr)::method) + TypeInfo exprType = resolveNodeType(target); + if (exprType != null && exprType.isResolved()) { + // Expression results are always instance references + return new MethodReferenceTarget(exprType, false); + } + + return null; + } + + /** + * Resolve a simple identifier as method reference target. + */ + private MethodReferenceTarget resolveSimpleTarget(String name) { + // this::method - instance method on current object + if ("this".equals(name)) { + TypeInfo thisType = context.resolveIdentifier("this"); + return thisType != null ? new MethodReferenceTarget(thisType, false) : null; + } + + // super::method - instance method on superclass + if ("super".equals(name)) { + TypeInfo superType = context.resolveIdentifier("super"); + return superType != null ? new MethodReferenceTarget(superType, false) : null; + } + + // Try as type name first (ClassName::method) + TypeInfo typeInfo = context.resolveTypeName(name); + if (typeInfo != null && typeInfo.isResolved()) { + // Wrap as ClassTypeInfo to indicate class reference + TypeInfo classRef = new ClassTypeInfo(typeInfo); + return new MethodReferenceTarget(classRef, true); + } + + // Try as variable (instance::method) + TypeInfo varType = context.resolveIdentifier(name); + if (varType != null && varType.isResolved()) { + // Check if variable holds a Class reference (e.g., var File = Java.type("java.io.File")) + boolean isClassRef = varType.isClassReference(); + return new MethodReferenceTarget(varType, isClassRef); + } + + return null; + } + + /** + * Resolve a qualified name (a.b.c) as method reference target. + * Handles both package-qualified class names and nested class access. + */ + private MethodReferenceTarget resolveQualifiedTarget(ExpressionNode.MemberAccessNode memberAccess) { + // Try to build qualified name and resolve as type + String qualifiedName = buildQualifiedName(memberAccess); + if (qualifiedName != null) { + TypeInfo typeInfo = context.resolveTypeName(qualifiedName); + if (typeInfo != null && typeInfo.isResolved()) { + TypeInfo classRef = new ClassTypeInfo(typeInfo); + return new MethodReferenceTarget(classRef, true); + } } - // Check if expected type is a functional interface - if (!isFunctionalInterface(expectedType)) { - // Not a functional interface + // Try resolving as expression chain (object.field::method) + TypeInfo exprType = resolveNodeType(memberAccess); + if (exprType != null && exprType.isResolved()) { + boolean isClassRef = exprType.isClassReference(); + return new MethodReferenceTarget(exprType, isClassRef); + } + + return null; + } + + /** + * Build a qualified name string from a MemberAccessNode chain. + * Example: a.b.c.ClassName -> "a.b.c.ClassName" + */ + private String buildQualifiedName(ExpressionNode node) { + if (node instanceof ExpressionNode.IdentifierNode) { + return ((ExpressionNode.IdentifierNode) node).getName(); + } + + if (node instanceof ExpressionNode.MemberAccessNode) { + ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node; + String baseName = buildQualifiedName(ma.getTarget()); + if (baseName != null) { + return baseName + "." + ma.getMemberName(); + } + } + + return null; + } + + /** + * Resolve a method reference (target::methodName). + */ + private TypeInfo resolveMethodReference(MethodReferenceTarget target, String methodName, + MethodInfo sam, TypeInfo expectedType) { + TypeInfo targetType = target.isClassRef && target.type instanceof ClassTypeInfo + ? ((ClassTypeInfo) target.type).getInstanceType() + : target.type; + + if (targetType == null || !targetType.hasMethod(methodName)) { return TypeInfo.fromClass(Object.class); } - // For now, we'll do basic validation and return the expected type - // In a full implementation, we would: - // 1. Resolve the target type (left side of ::) - // 2. Find the referenced method on the target type - // 3. Extract SAM signature from expected type - // 4. Validate parameter/return type compatibility + // Find best matching method using OverloadSelector + MethodInfo method = findBestMethodForReference(targetType, methodName, sam, target.isClassRef); + if (method == null) { + return TypeInfo.fromClass(Object.class); + } + + // Validate signature compatibility + String error = validateMethodSignature(method, sam, target.isClassRef, targetType); + if (error != null) { + return TypeInfo.fromClass(Object.class); + } - // Basic implementation: assume valid and return expected type return expectedType; } - private boolean isFunctionalInterface(TypeInfo type) { - // Check if the type has exactly one abstract method (SAM type) - // Common functional interfaces: Runnable, Supplier, Consumer, Function, Predicate, etc. - if (type == null) return false; + /** + * Find the best matching method for a method reference using OverloadSelector logic. + */ + private MethodInfo findBestMethodForReference(TypeInfo targetType, String methodName, + MethodInfo sam, boolean isClassRef) { + List allOverloads = targetType.getAllMethodOverloads(methodName); + if (allOverloads.isEmpty()) { + return null; + } + + // Extract SAM parameter types for overload matching + TypeInfo[] samParamTypes = extractSamParamTypes(sam, isClassRef, targetType); - String typeName = type.getFullName(); - if (typeName == null) return false; - - return typeName.equals("java.lang.Runnable") || - typeName.contains("Supplier") || - typeName.contains("Consumer") || - typeName.contains("Function") || - typeName.contains("Predicate") || - typeName.contains("BiFunction") || - typeName.contains("BiConsumer") || - typeName.contains("UnaryOperator") || - typeName.contains("BinaryOperator"); + // Filter candidates by arity first, then use OverloadSelector + List candidates = new ArrayList<>(); + int expectedArity = samParamTypes.length; + + for (MethodInfo method : allOverloads) { + int methodArity = method.getParameters().size(); + + // Direct match: instance::method or ClassName::staticMethod + if (methodArity == expectedArity) { + candidates.add(method); + } + // Unbound instance method: ClassName::instanceMethod (first SAM param is receiver) + else if (isClassRef && methodArity == sam.getParameters().size() - 1 && !isStaticMethod(method)) { + candidates.add(method); + } + } + + if (candidates.isEmpty()) { + return null; + } + + if (candidates.size() == 1) { + return candidates.get(0); + } + + // Use OverloadSelector for multi-candidate selection + return OverloadSelector.selectBestOverload(candidates, samParamTypes); + } + + /** + * Extract the effective parameter types from SAM for method matching. + * For unbound instance methods, excludes the first receiver parameter. + */ + private TypeInfo[] extractSamParamTypes(MethodInfo sam, boolean isClassRef, TypeInfo targetType) { + List samParams = sam.getParameters(); + + // For class references, we might be matching an unbound instance method + // where the first SAM param is the receiver - handled in findBestMethodForReference + TypeInfo[] types = new TypeInfo[samParams.size()]; + for (int i = 0; i < samParams.size(); i++) { + types[i] = samParams.get(i).getTypeInfo(); + } + return types; + } + + /** + * Check if a method is static (Java reflection). + */ + private boolean isStaticMethod(MethodInfo method) { + java.lang.reflect.Method javaMethod = method.getJavaMethod(); + if (javaMethod != null) { + return java.lang.reflect.Modifier.isStatic(javaMethod.getModifiers()); + } + // For script-defined methods, assume non-static unless marked + return method.isStatic(); + } + + /** + * Validate method signature compatibility with SAM. + */ + private String validateMethodSignature(MethodInfo method, MethodInfo sam, + boolean isClassRef, TypeInfo targetType) { + List methodParams = method.getParameters(); + List samParams = sam.getParameters(); + + int methodArity = methodParams.size(); + int samArity = samParams.size(); + + // Check for unbound instance method reference (ClassName::instanceMethod) + boolean isUnbound = isClassRef && methodArity == samArity - 1 && !isStaticMethod(method); + + // Validate parameter count + if (methodArity != samArity && !isUnbound) { + return "Parameter count mismatch"; + } + + // Validate parameter types + int offset = isUnbound ? 1 : 0; + for (int i = 0; i < methodArity; i++) { + TypeInfo methodParamType = methodParams.get(i).getTypeInfo(); + TypeInfo samParamType = samParams.get(i + offset).getTypeInfo(); + + if (methodParamType != null && samParamType != null) { + if (!TypeChecker.isTypeCompatible(methodParamType, samParamType)) { + return "Parameter type mismatch at position " + (i + 1); + } + } + } + + // For unbound reference, validate receiver compatibility + if (isUnbound && samArity > 0) { + TypeInfo firstSamParam = samParams.get(0).getTypeInfo(); + if (firstSamParam != null && !TypeChecker.isTypeCompatible(targetType, firstSamParam)) { + return "Receiver type mismatch"; + } + } + + // Validate return type (covariant) + TypeInfo methodReturn = method.getReturnType(); + TypeInfo samReturn = sam.getReturnType(); + + if (samReturn != null && methodReturn != null) { + boolean samIsVoid = "void".equals(samReturn.getFullName()) || samReturn.getJavaClass() == void.class; + if (!samIsVoid && !TypeChecker.isTypeCompatible(samReturn, methodReturn)) { + return "Return type mismatch"; + } + } + + return null; + } + + /** + * Resolve a constructor reference (ClassName::new or Type[]::new). + */ + private TypeInfo resolveConstructorReference(MethodReferenceTarget target, MethodInfo sam, + TypeInfo expectedType) { + TypeInfo targetType = target.type; + + // Handle array constructor reference: Type[]::new + if (isArrayType(targetType)) { + return resolveArrayConstructorReference(targetType, sam, expectedType); + } + + // Get the actual class type for constructor lookup + TypeInfo classType = target.type instanceof ClassTypeInfo + ? ((ClassTypeInfo) target.type).getInstanceType() + : target.type; + + if (classType == null) { + return TypeInfo.fromClass(Object.class); + } + + // Cannot construct interfaces or abstract classes + if (classType.getKind() == TypeInfo.Kind.INTERFACE) { + return TypeInfo.fromClass(Object.class); + } + + // Extract SAM parameter types for constructor matching + List samParams = sam.getParameters(); + TypeInfo[] paramTypes = new TypeInfo[samParams.size()]; + for (int i = 0; i < samParams.size(); i++) { + paramTypes[i] = samParams.get(i).getTypeInfo(); + } + + // Find matching constructor + MethodInfo constructor = classType.findConstructor(paramTypes); + if (constructor == null && samParams.size() > 0) { + // Try by arity if exact type match fails + constructor = classType.findConstructor(samParams.size()); + } + + if (constructor == null) { + return TypeInfo.fromClass(Object.class); + } + + // Validate return type compatibility + TypeInfo samReturn = sam.getReturnType(); + if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, classType)) { + return TypeInfo.fromClass(Object.class); + } + + return expectedType; + } + + /** + * Resolve an array constructor reference: Type[]::new + * Must match IntFunction or similar single-int-param functional interface. + */ + private TypeInfo resolveArrayConstructorReference(TypeInfo arrayType, MethodInfo sam, + TypeInfo expectedType) { + List samParams = sam.getParameters(); + + // Array constructor takes exactly one int parameter (the size) + if (samParams.size() != 1) { + return TypeInfo.fromClass(Object.class); + } + + TypeInfo sizeParam = samParams.get(0).getTypeInfo(); + if (sizeParam == null || !isIntLike(sizeParam)) { + return TypeInfo.fromClass(Object.class); + } + + // Return type must be compatible with array type + TypeInfo samReturn = sam.getReturnType(); + if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, arrayType)) { + return TypeInfo.fromClass(Object.class); + } + + return expectedType; + } + + /** + * Check if a type is an array type. + */ + private boolean isArrayType(TypeInfo type) { + if (type == null) return false; + String name = type.getFullName(); + return name != null && name.endsWith("[]"); + } + + /** + * Check if a type is int-like (int, Integer, or numeric that can represent array size). + */ + private boolean isIntLike(TypeInfo type) { + if (type == null) return false; + String name = type.getFullName(); + return "int".equals(name) || "java.lang.Integer".equals(name) + || "long".equals(name) || "java.lang.Long".equals(name); } public static ExpressionNode.TypeResolverContext createBasicContext() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java index ec396b7bb..7480ad466 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodCallInfo.java @@ -382,6 +382,13 @@ public void validateArgTypeError() { // Check each argument type for (int i = 0; i < arguments.size(); i++) { Argument arg = arguments.get(i); + + // Surface argument-level errors (e.g., SAM conflict, ambiguous overload, bare method in Java) + if (!arg.isValid() && arg.getErrorMessage() != null) { + setArgTypeError(i, arg.getErrorMessage()); + continue; // Skip remaining checks for this argument + } + FieldInfo para = resolvedMethod.getParameters().get(i); TypeInfo argType = arg.getResolvedType(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java index 886b1bb54..d780859d8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/method/MethodInfo.java @@ -43,7 +43,8 @@ public enum ErrorType { VOID_METHOD_RETURNS_VALUE, // Void method returns a value DUPLICATE_METHOD, // Method with same signature already defined in scope DUPLICATE_PARAMETER, // Two parameters have the same name - PARAMETER_UNDEFINED // Parameter type cannot be resolved + PARAMETER_UNDEFINED, // Parameter type cannot be resolved + SAM_TYPE_INCOMPATIBLE // Explicit param type incompatible with SAM context } private final String name; @@ -549,6 +550,16 @@ public void setError(ErrorType type, String message) { public void addParameterError(FieldInfo param, int index, ErrorType type, String message) { parameterErrors.add(new ParameterError(param, index, type, message)); } + + public void addSamTypeError(int paramIndex, TypeInfo expectedSamType, TypeInfo actualDeclaredType) { + if (paramIndex < 0 || paramIndex >= parameters.size()) { + return; + } + FieldInfo param = parameters.get(paramIndex); + String message = "Explicit type '" + actualDeclaredType.getSimpleName() + + "' is incompatible with SAM parameter type '" + expectedSamType.getSimpleName() + "'"; + parameterErrors.add(new ParameterError(param, paramIndex, ErrorType.SAM_TYPE_INCOMPATIBLE, message)); + } /** * Add a return statement error. diff --git a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java index e9164f20f..70c6b73f4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/interpreter/type/TypeChecker.java @@ -17,6 +17,12 @@ private TypeChecker() {} // Utility class public static boolean isTypeCompatible(TypeInfo expected, TypeInfo actual) { if (expected == null) return true; // void can accept anything (shouldn't happen) if (actual == null) return true; // Can't verify, assume compatible + + // Script method reference placeholder: only compatible with functional interface params. + // Used to help overload selection choose SAM overloads in JavaScript. + if ("__script_method_ref__".equals(actual.getFullName())) { + return expected.isFunctionalInterface(); + } // Handle "any" type - universally compatible (JavaScript/TypeScript) if ("any".equals(expected.getFullName()) || "any".equals(actual.getFullName())) { @@ -343,4 +349,3 @@ public static String[] getJaveScriptKeywords() { return keywords; } } - From 430ff7c3a83b29abea920c9e1e249771a8ec3858 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 18 Feb 2026 05:21:55 +0200 Subject: [PATCH 336/337] Improve autocomplete for method reference contexts - trigger completion after :: and detect method-reference receiver chains - restrict suggestions to methods (plus constructor refs) in method-reference mode - insert method names without parentheses when completing method references --- .../script/autocomplete/AutocompleteItem.java | 66 +++++-- .../autocomplete/AutocompleteManager.java | 164 +++++++++++++++++- .../autocomplete/AutocompleteProvider.java | 11 ++ .../autocomplete/JSAutocompleteProvider.java | 52 ++++-- .../JavaAutocompleteProvider.java | 89 +++++++--- 5 files changed, 326 insertions(+), 56 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java index fdbf3cf86..707a1e664 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteItem.java @@ -85,15 +85,27 @@ private AutocompleteItem(String name, String searchName, String insertText, Kind * Create from a Java MethodInfo. */ public static AutocompleteItem fromMethod(MethodInfo method) { + return fromMethod(method, false); + } + + /** + * Create from a Java MethodInfo, optionally for method reference context. + * @param method The method info + * @param forMethodReference If true, insert text will NOT include parentheses + */ + public static AutocompleteItem fromMethod(MethodInfo method, boolean forMethodReference) { String name = method.getName(); StringBuilder insertText = new StringBuilder(name); - insertText.append("("); - // Add placeholders for parameters if any - if (method.getParameterCount() > 0) { - // Just add () - user will fill in params + // For method references, don't add () since it's ::methodName not ::methodName() + if (!forMethodReference) { + insertText.append("("); + // Add placeholders for parameters if any + if (method.getParameterCount() > 0) { + // Just add () - user will fill in params + } + insertText.append(")"); } - insertText.append(")"); String returnType = method.getReturnType() != null ? getName(method.getReturnType()) : "void"; @@ -208,7 +220,7 @@ public static AutocompleteItem fromType(TypeInfo type) { * Create from a JavaScript JSMethodInfo. */ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { - return fromJSMethod(method, null, 0); + return fromJSMethod(method, null, 0, false); } /** @@ -217,7 +229,7 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method) { * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) */ public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritanceDepth) { - return fromJSMethod(method, null, inheritanceDepth); + return fromJSMethod(method, null, inheritanceDepth, false); } /** @@ -227,10 +239,25 @@ public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritance * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) */ public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contextType, int inheritanceDepth) { + return fromJSMethod(method, contextType, inheritanceDepth, false); + } + + /** + * Create from a JavaScript JSMethodInfo with type parameter resolution for display. + * @param method The method info + * @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP) + * @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.) + * @param forMethodReference If true, insert text will NOT include parentheses + */ + public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contextType, int inheritanceDepth, boolean forMethodReference) { String name = method.getName(); StringBuilder insertText = new StringBuilder(name); - insertText.append("("); - insertText.append(")"); + + // For method references, don't add () since it's ::methodName not ::methodName() + if (!forMethodReference) { + insertText.append("("); + insertText.append(")"); + } // Build display name with resolved parameter types StringBuilder displayName = new StringBuilder(name); @@ -338,10 +365,27 @@ public static AutocompleteItem keyword(String keyword) { public static AutocompleteItem fromSyntheticMethod( SyntheticMethod method, TypeInfo containingType) { + return fromSyntheticMethod(method, containingType, false); + } + + /** + * Create from a synthetic method (e.g., Nashorn built-in like Java.type). + * @param method The synthetic method + * @param containingType The containing type info + * @param forMethodReference If true, insert text will NOT include parentheses + */ + public static AutocompleteItem fromSyntheticMethod( + SyntheticMethod method, + TypeInfo containingType, + boolean forMethodReference) { String name = method.name; StringBuilder insertText = new StringBuilder(name); - insertText.append("("); - insertText.append(")"); + + // For method references, don't add () since it's ::methodName not ::methodName() + if (!forMethodReference) { + insertText.append("("); + insertText.append(")"); + } // Build display name with parameters - use simple names for types StringBuilder displayName = new StringBuilder(name); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java index d6196edc4..20cde85cc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteManager.java @@ -177,6 +177,21 @@ public void onCharTyped(char c, String text, int cursorPosition) { return; } + // Check if this completes a :: (method reference) + if (c == ':') { + // Normalize cursor position - some callers pass pre-insert position + int effectiveCursor = cursorPosition; + if (effectiveCursor >= 0 && effectiveCursor < text.length() && text.charAt(effectiveCursor) == c) { + effectiveCursor = effectiveCursor + 1; + } + + // Check if the previous character is also : + if (effectiveCursor >= 2 && text.charAt(effectiveCursor - 2) == ':') { + triggerAfterMethodReference(text, effectiveCursor); + return; + } + } + // Check if we're typing an identifier if (Character.isJavaIdentifierPart(c)) { // GuiScriptTextArea passes a cursor position captured BEFORE insertion while `text` @@ -324,7 +339,100 @@ private void triggerAfterDot(String text, int cursorPosition) { prefixStartPosition = prefixStart; currentPrefix = prefix; - showSuggestions(text, caretPos, prefix, prefixStartPosition, true, receiverExpr); + showSuggestions(text, caretPos, prefix, prefixStartPosition, true, receiverExpr, false); + } + + /** + * Trigger autocomplete after :: is typed (method reference). + * Shows only method suggestions filtered for method references. + */ + private void triggerAfterMethodReference(String text, int cursorPosition) { + int doubleColonPos = cursorPosition - 2; + + // Find the receiver expression before :: + String receiverExpr = findMethodRefReceiverExpression(text, doubleColonPos); + if (receiverExpr == null || receiverExpr.isEmpty()) return; + + // Skip whitespace after :: when establishing prefix start position + int prefixStart = cursorPosition; + while (prefixStart < text.length() && Character.isWhitespace(text.charAt(prefixStart))) { + prefixStart++; + } + + String prefix = ""; + + prefixStartPosition = prefixStart; + currentPrefix = prefix; + + // Show suggestions filtered for methods only + showSuggestions(text, cursorPosition, prefix, prefixStart, true, receiverExpr, true); + } + + /** + * Find the receiver expression before :: for method references. + * Similar to findReceiverExpression but stops at :: + */ + private String findMethodRefReceiverExpression(String text, int doubleColonPos) { + if (text == null || doubleColonPos <= 0) return ""; + + int pos = doubleColonPos - 1; + + // Skip whitespace immediately left of :: + while (pos >= 0 && Character.isWhitespace(text.charAt(pos))) pos--; + if (pos < 0) return ""; + + int end = pos + 1; + + // Walk left across a dotted receiver chain + while (pos >= 0) { + char c = text.charAt(pos); + + if (Character.isWhitespace(c)) { + pos--; + continue; + } + + if (c == ')') { + int open = findMatchingBackward(text, pos, '(', ')'); + if (open < 0) break; + pos = open - 1; + continue; + } + + if (c == ']') { + int open = findMatchingBackward(text, pos, '[', ']'); + if (open < 0) break; + pos = open - 1; + continue; + } + + if (Character.isJavaIdentifierPart(c)) { + // Consume identifier + while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) pos--; + + // If preceded by a dot, keep going + int checkPos = pos; + while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) { + checkPos--; + } + if (checkPos >= 0 && text.charAt(checkPos) == '.') { + pos = checkPos - 1; + continue; + } + break; + } + + // Anything else terminates the receiver + break; + } + + int start = pos + 1; + if (start < 0) start = 0; + if (end > text.length()) end = text.length(); + if (start >= end) return ""; + + String expr = text.substring(start, end).replaceAll("\\s+", " ").trim(); + return expr; } /** @@ -347,7 +455,7 @@ private void maybeStartAutocomplete(String text, int cursorPosition, boolean for prefixStartPosition = prefixStart; currentPrefix = prefix; - showSuggestions(text, cursorPosition, prefix, prefixStart, false, null); + showSuggestions(text, cursorPosition, prefix, prefixStart, false, null, false); } /** @@ -370,17 +478,58 @@ private void updatePrefix(String text, int cursorPosition) { currentPrefix = newPrefix; + // Check if this is a method reference context (after ::) + boolean isMethodReference = isAfterMethodReference(text, prefixStartPosition); + // Check if after dot (skipping whitespace) int dotPos = findDotBeforeWhitespace(text, prefixStartPosition - 1); - boolean isMemberAccess = dotPos >= 0; + boolean isMemberAccess = dotPos >= 0 || isMethodReference; String receiverExpr = null; - if (isMemberAccess) { + if (isMethodReference) { + // Find :: position + int doubleColonPos = findDoubleColonBefore(text, prefixStartPosition); + if (doubleColonPos >= 0) { + receiverExpr = findMethodRefReceiverExpression(text, doubleColonPos); + } + } else if (isMemberAccess) { receiverExpr = findReceiverExpression(text, dotPos); } showSuggestions(text, cursorPosition, currentPrefix, prefixStartPosition, - isMemberAccess, receiverExpr); + isMemberAccess, receiverExpr, isMethodReference); + } + + /** + * Check if position is after :: (method reference). + */ + private boolean isAfterMethodReference(String text, int pos) { + int checkPos = pos - 1; + // Skip whitespace + while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) { + checkPos--; + } + // Check for :: + if (checkPos >= 1 && text.charAt(checkPos) == ':' && text.charAt(checkPos - 1) == ':') { + return true; + } + return false; + } + + /** + * Find the position of :: before the given position. + */ + private int findDoubleColonBefore(String text, int pos) { + int checkPos = pos - 1; + // Skip whitespace + while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) { + checkPos--; + } + // Check for :: + if (checkPos >= 1 && text.charAt(checkPos) == ':' && text.charAt(checkPos - 1) == ':') { + return checkPos - 1; + } + return -1; } // ==================== SUGGESTION LOGIC ==================== @@ -389,7 +538,8 @@ private void updatePrefix(String text, int cursorPosition) { * Show autocomplete suggestions. */ private void showSuggestions(String text, int cursorPosition, String prefix, - int prefixStart, boolean isMemberAccess, String receiverExpr) { + int prefixStart, boolean isMemberAccess, String receiverExpr, + boolean methodsOnly) { if (insertCallback == null || document == null) return; // Track context for usage recording when item is selected @@ -416,7 +566,7 @@ private void showSuggestions(String text, int cursorPosition, String prefix, AutocompleteProvider.Context context = new AutocompleteProvider.Context( text, cursorPosition, lineNumber, columnPosition, currentLine, - prefix, prefixStart, isMemberAccess, receiverExpr, explicitTrigger + prefix, prefixStart, isMemberAccess, receiverExpr, explicitTrigger, methodsOnly ); // Get suggestions from appropriate provider diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java index 1ada3614a..a11282d24 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/AutocompleteProvider.java @@ -32,10 +32,20 @@ class Context { public final String receiverExpression; /** Whether autocomplete was explicitly triggered (CTRL+Space) */ public final boolean explicitTrigger; + /** Whether to show only methods (for method reference context ::) */ + public final boolean methodsOnly; public Context(String text, int cursorPosition, int lineNumber, int columnPosition, String currentLine, String prefix, int prefixStart, boolean isMemberAccess, String receiverExpression, boolean explicitTrigger) { + this(text, cursorPosition, lineNumber, columnPosition, currentLine, prefix, prefixStart, + isMemberAccess, receiverExpression, explicitTrigger, false); + } + + public Context(String text, int cursorPosition, int lineNumber, int columnPosition, + String currentLine, String prefix, int prefixStart, + boolean isMemberAccess, String receiverExpression, boolean explicitTrigger, + boolean methodsOnly) { this.text = text; this.cursorPosition = cursorPosition; this.lineNumber = lineNumber; @@ -46,6 +56,7 @@ public Context(String text, int cursorPosition, int lineNumber, int columnPositi this.isMemberAccess = isMemberAccess; this.receiverExpression = receiverExpression; this.explicitTrigger = explicitTrigger; + this.methodsOnly = methodsOnly; } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java index 1ba3d9769..0cd3f6418 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/autocomplete/JSAutocompleteProvider.java @@ -49,7 +49,7 @@ protected void addMemberSuggestions(Context context, List item SyntheticType syntheticType = document.getTypeResolver().getSyntheticType(receiverType.getSimpleName()); if (syntheticType != null) { - addSyntheticTypeSuggestions(syntheticType, items); + addSyntheticTypeSuggestions(syntheticType, items, context.methodsOnly); return; } @@ -65,25 +65,32 @@ protected void addMemberSuggestions(Context context, List item JSTypeInfo jsTypeInfo = receiverType.getJSTypeInfo(); if (jsTypeInfo != null) { // Pass both: jsTypeInfo (current type in hierarchy) and receiverType (context for type params) - addMethodsFromType(jsTypeInfo, receiverType, items, new HashSet<>()); - addFieldsFromType(jsTypeInfo, receiverType, items, new HashSet<>()); + addMethodsFromType(jsTypeInfo, receiverType, items, new HashSet<>(), context.methodsOnly); + if (!context.methodsOnly) { + addFieldsFromType(jsTypeInfo, receiverType, items, new HashSet<>()); + } } } /** * Add autocomplete suggestions for a synthetic type's methods and fields. + * @param syntheticType The synthetic type + * @param items The list to add items to + * @param forMethodReference If true, only add methods with no parentheses in insert text */ - private void addSyntheticTypeSuggestions(SyntheticType syntheticType, List items) { + private void addSyntheticTypeSuggestions(SyntheticType syntheticType, List items, boolean forMethodReference) { // Add methods for (SyntheticMethod method : syntheticType.getMethods()) { - AutocompleteItem item = AutocompleteItem.fromSyntheticMethod(method, syntheticType.getTypeInfo()); + AutocompleteItem item = AutocompleteItem.fromSyntheticMethod(method, syntheticType.getTypeInfo(), forMethodReference); items.add(item); } - // Add fields - for (SyntheticField field : syntheticType.getFields()) { - AutocompleteItem item = AutocompleteItem.fromSyntheticField(field); - items.add(item); + // Add fields (skip if forMethodReference is true) + if (!forMethodReference) { + for (SyntheticField field : syntheticType.getFields()) { + AutocompleteItem item = AutocompleteItem.fromSyntheticField(field); + items.add(item); + } } } @@ -93,7 +100,17 @@ private void addSyntheticTypeSuggestions(SyntheticType syntheticType, List items, Set added) { - addMethodsFromType(type, contextType, items, added, 0); + addMethodsFromType(type, contextType, items, added, 0, false); + } + + /** + * Recursively add methods from a type and its parents. + * @param type The current JS type to get methods from (changes as we walk up inheritance) + * @param contextType The original TypeInfo context for resolving type parameters (stays constant) + * @param forMethodReference If true, insert text will NOT include parentheses + */ + protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added, boolean forMethodReference) { + addMethodsFromType(type, contextType, items, added, 0, forMethodReference); } /** @@ -103,6 +120,17 @@ protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added, int depth) { + addMethodsFromType(type, contextType, items, added, depth, false); + } + + /** + * Recursively add methods from a type and its parents with inheritance depth tracking. + * Shows all overloads - one autocomplete item per overload. + * @param type The current JS type in inheritance chain + * @param contextType The original TypeInfo context for resolving type parameters (e.g., IPlayer with T → EntityPlayerMP) + * @param forMethodReference If true, insert text will NOT include parentheses + */ + private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List items, Set added, int depth, boolean forMethodReference) { // Collect base method names (without $N suffix) that we haven't processed yet Set baseNames = new HashSet<>(); for (String key : type.getMethods().keySet()) { @@ -127,13 +155,13 @@ private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List getSuggestions(Context context) { addScopeSuggestions(context, items); } + // For method reference context, filter to show only methods (and constructor "new") + if (context.methodsOnly) { + items.removeIf(item -> item.getKind() != AutocompleteItem.Kind.METHOD); + + // Add "new" constructor reference option if the receiver is a class + if (context.receiverExpression != null) { + int resolvePos = getMemberAccessResolvePosition(context); + TypeInfo receiverType = document.resolveExpressionType( + context.receiverExpression, resolvePos); + if (receiverType != null && receiverType.isResolved() && isStaticContext) { + items.add(new AutocompleteItem.Builder() + .name("new") + .insertText("new") + .kind(AutocompleteItem.Kind.KEYWORD) + .typeLabel("constructor") + .signature(receiverType.getSimpleName() + "::new") + .build()); + } + } + } + // Filter and score by prefix, then apply usage boosts and static penalties filterAndScore(items, context.prefix, context.isMemberAccess, isStaticContext, ownerFullName); @@ -93,7 +114,7 @@ protected void addMemberSuggestions(Context context, List item if (clazz == null) { // Try ScriptTypeInfo if (receiverType instanceof ScriptTypeInfo) { - addScriptTypeMembers((ScriptTypeInfo) receiverType, items, isStaticContext); + addScriptTypeMembers((ScriptTypeInfo) receiverType, items, isStaticContext, context.methodsOnly); } return; } @@ -111,21 +132,23 @@ protected void addMemberSuggestions(Context context, List item if (!addedMethods.contains(sig)) { addedMethods.add(sig); MethodInfo methodInfo = MethodInfo.fromReflection(method, receiverType); - items.add(AutocompleteItem.fromMethod(methodInfo)); + items.add(AutocompleteItem.fromMethod(methodInfo, context.methodsOnly)); } } } - // Add fields - for (Field field : clazz.getFields()) { - if (Modifier.isPublic(field.getModifiers())) { - // Filter by static context - if (isStaticContext && !Modifier.isStatic(field.getModifiers())) { - continue; + // Add fields (skip if methodsOnly is true - but the filtering will be done in getSuggestions) + if (!context.methodsOnly) { + for (Field field : clazz.getFields()) { + if (Modifier.isPublic(field.getModifiers())) { + // Filter by static context + if (isStaticContext && !Modifier.isStatic(field.getModifiers())) { + continue; + } + + FieldInfo fieldInfo = FieldInfo.fromReflection(field, receiverType); + items.add(AutocompleteItem.fromField(fieldInfo)); } - - FieldInfo fieldInfo = FieldInfo.fromReflection(field, receiverType); - items.add(AutocompleteItem.fromField(fieldInfo)); } } @@ -165,6 +188,17 @@ protected int getMemberAccessResolvePosition(Context context) { * Add members from a script-defined type. */ protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List items, boolean isStaticContext) { + addScriptTypeMembers(scriptType, items, isStaticContext, false); + } + + /** + * Add members from a script-defined type. + * @param scriptType The script type to get members from + * @param items The list to add items to + * @param isStaticContext Whether we're in a static context + * @param forMethodReference If true, only add methods with no parentheses in insert text + */ + protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List items, boolean isStaticContext, boolean forMethodReference) { // Add methods (getMethods returns Map>) for (List overloads : scriptType.getMethods().values()) { for (MethodInfo method : overloads) { @@ -172,22 +206,25 @@ protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List) - for (FieldInfo field : scriptType.getFields().values()) { - // Filter by static context - if (isStaticContext && !field.isStatic()) { - continue; + // Add fields (skip if methodsOnly/forMethodReference is true) + if (!forMethodReference) { + // Add fields (getFields returns Map) + for (FieldInfo field : scriptType.getFields().values()) { + // Filter by static context + if (isStaticContext && !field.isStatic()) { + continue; + } + items.add(AutocompleteItem.fromField(field)); + } + + // Add enum constants (getEnumConstants returns Map) + for (EnumConstantInfo enumConstant : scriptType.getEnumConstants().values()) { + items.add(AutocompleteItem.fromField(enumConstant.getFieldInfo())); } - items.add(AutocompleteItem.fromField(field)); - } - - // Add enum constants (getEnumConstants returns Map) - for (EnumConstantInfo enumConstant : scriptType.getEnumConstants().values()) { - items.add(AutocompleteItem.fromField(enumConstant.getFieldInfo())); } // Add parent class members @@ -202,7 +239,7 @@ protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List items // Add methods from enclosing type for (List overloads : enclosingType.getMethods().values()) { for (MethodInfo method : overloads) { - items.add(AutocompleteItem.fromMethod(method)); + items.add(AutocompleteItem.fromMethod(method, context.methodsOnly)); } } } // Add script-defined methods for (MethodInfo method : document.getAllMethods()) { - items.add(AutocompleteItem.fromMethod(method)); + items.add(AutocompleteItem.fromMethod(method, context.methodsOnly)); } addLanguageUniqueSuggestions(context, items); From 4764b8eb3028f1a18046319391ad6dd3ae7c0c32 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 19 Feb 2026 22:24:31 +0200 Subject: [PATCH 337/337] Enabled script console --- src/main/java/noppes/npcs/controllers/ScriptContainer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index dbeddf407..efdbc5b2d 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -153,7 +153,7 @@ public void run(ScriptEngine engine) { var14.printStackTrace(pw); } finally { String errorString = sw.getBuffer().toString().trim(); - // this.appendConsole(errorString); + this.appendConsole(errorString); pw.close(); } } @@ -228,7 +228,7 @@ public void run(String type, Object event) { errored = true; e.printStackTrace(pw); } finally { - // appendConsole(sw.getBuffer().toString().trim()); + appendConsole(sw.getBuffer().toString().trim()); pw.close(); Current = null; }