From 71b4a6fdc4225743eb2f202a84508a9c0dada7cc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 31 Dec 2025 18:53:28 +0200 Subject: [PATCH] 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.Minecraft; 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 @@ 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.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 @@ public class GuiScriptTextArea extends GuiNpcTextField { 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 class GuiScriptTextArea extends GuiNpcTextField { 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 class GuiScriptTextArea extends GuiNpcTextField { 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 class GuiScriptTextArea extends GuiNpcTextField { } 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 class GuiScriptTextArea extends GuiNpcTextField { } } } + + // 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 @@ public class GuiScriptTextArea extends GuiNpcTextField { 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 class GuiUtil { 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 @@ public class ScriptDocument { // Parse and validate assignments (reassignments) - stores in FieldInfo parseAssignments(); + + // Detect method overrides and interface implementations for script types + detectMethodInheritance(); } /** @@ -356,11 +359,12 @@ public class ScriptDocument { * 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 @@ public class ScriptDocument { 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 @@ public class ScriptDocument { 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 @@ public class ScriptDocument { * 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 @@ public class ScriptDocument { } 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 @@ public class ScriptDocument { } 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 @@ public class ScriptDocument { // 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 @@ public class TokenHoverInfo { : 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 @@ public class TokenHoverInfo { } /** - * 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 final class MethodInfo { 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 final class MethodInfo { 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 class ScriptTypeInfo extends TypeInfo { 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 class ScriptTypeInfo extends TypeInfo { 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