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 class ScriptLine { /** * 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() + ); } /**