mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 21:17:32 +00:00
Code logic improvements
This commit is contained in:
1 parent
457a8aae37
commit
9cb4c96eb3
9 files changed
+1773
-106
No files matched your search
@@ -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<Token> 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 final class ImportData {
|
||||
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<Token> 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.
|
||||
|
||||
@@ -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<Argument> 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<ArgumentTypeError> argumentTypeErrors = new ArrayList<>();
|
||||
|
||||
public MethodCallInfo(String methodName, int methodNameStart, int methodNameEnd,
|
||||
int openParenOffset, int closeParenOffset,
|
||||
List<Argument> 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<Argument> 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<Argument> 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<FieldInfo> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<FieldInfo> 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 @@ public final class MethodInfo {
|
||||
this.bodyEnd = bodyEnd;
|
||||
this.resolved = resolved;
|
||||
this.isDeclaration = isDeclaration;
|
||||
this.isStatic = isStatic;
|
||||
}
|
||||
|
||||
// Factory methods
|
||||
public static MethodInfo declaration(String name, TypeInfo returnType, List<FieldInfo> 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<FieldInfo> 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 final class MethodInfo {
|
||||
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 final class MethodInfo {
|
||||
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 final class MethodInfo {
|
||||
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<FieldInfo> params = new ArrayList<>();
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
@@ -73,7 +83,7 @@ public final class MethodInfo {
|
||||
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 final class MethodInfo {
|
||||
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.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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 class ScriptLine {
|
||||
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 class ScriptLine {
|
||||
/**
|
||||
* 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<int[]> 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 class ScriptLine {
|
||||
.append(COLOR_CHAR)
|
||||
.append('f'); // Reset to white
|
||||
|
||||
currentX += ClientProxy.Font.width(t.getText());
|
||||
lastIndex = tokenStart + t.getText().length();
|
||||
}
|
||||
|
||||
@@ -218,16 +250,67 @@ public class ScriptLine {
|
||||
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 class ScriptLine {
|
||||
// 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 ====================
|
||||
|
||||
@@ -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<String, FieldInfo> fields = new HashMap<>();
|
||||
private final Map<String, List<MethodInfo>> methods = new HashMap<>(); // name -> list of overloads
|
||||
private final List<ScriptTypeInfo> 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<ScriptTypeInfo> 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<String, FieldInfo> 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<MethodInfo> 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<MethodInfo> overloads = methods.get(methodName);
|
||||
return (overloads != null && !overloads.isEmpty()) ? overloads.get(0) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all method overloads with the given name.
|
||||
*/
|
||||
public List<MethodInfo> getMethodOverloads(String methodName) {
|
||||
return methods.getOrDefault(methodName, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a method with specific parameter count.
|
||||
*/
|
||||
public MethodInfo getMethodWithParamCount(String methodName, int paramCount) {
|
||||
List<MethodInfo> overloads = methods.get(methodName);
|
||||
if (overloads == null) return null;
|
||||
for (MethodInfo m : overloads) {
|
||||
if (m.getParameterCount() == paramCount) {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, List<MethodInfo>> 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() + "}";
|
||||
}
|
||||
}
|
||||
@@ -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 class Token {
|
||||
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 class Token {
|
||||
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; }
|
||||
|
||||
@@ -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 enum TokenType {
|
||||
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:
|
||||
|
||||
@@ -2,9 +2,9 @@ package noppes.npcs.client.gui.util.script.interpreter;
|
||||
|
||||
/**
|
||||
* 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 @@ public final class TypeInfo {
|
||||
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 final class TypeInfo {
|
||||
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; }
|
||||
|
||||
Reference in new issue
Block a user