Merge pull request #249 from KAMKEEL/syntax-highlighter-unified

Syntax Highlighter to Janino Scripts
This commit is contained in:
Kam authored and GitHub committed 2026-03-10 05:51:51 -04:00
commit f1e4aa5b6e
119 files changed
+45890 -912

No files matched your search

+2
View File
@@ -44,3 +44,5 @@ run
src/main/resources/assets/customnpcs/api
df/
tmp/
src/test
+92
View File
@@ -0,0 +1,92 @@
# CNPC+ 1.11 Beta - The Scripting Update
🟡 **In Testing**
## Features: ⚒️
- **Syntax Highlighting:**
> Full color-coded script editor. Keywords, types, variables, methods, strings, and comments are all distinct. Scope-aware coloring distinguishes local fields from globals and chained method calls.
- **Autocomplete:**
> Smart suggestions as you type. Context-aware for static vs instance access, shows method parameters, resolves chained calls, and suggests auto imports for classes. Frequently used members prioritized.
- **Error Detection:**
> Catch mistakes before running. Type mismatches, wrong arguments, missing methods, unused imports, duplicate definitions, and more -- all underlined with hover messages explaining the issue.
- **Hover Information:**
> Hover any symbol to see its type, documentation, and declaration. Works on methods, fields, classes, and even generics. Click to pin tooltips in place.
- **Script Editor Overhaul:**
> - **Line Numbers** - Gutter with current line highlighting.
> - **Go To Line** - CTRL+G to jump to any line.
> - **Search & Replace** - CTRL+F / CTRL+H with real-time highlighting.
> - **Go To Definition** - CTRL+Click to jump to any symbol.
> - **Smart Undo/Redo** - Word-level instead of character-level.
> - **Line Operations** - Duplicate, delete, move up/down.
> - **Comment Toggle** - CTRL+/ for selected lines.
> - **Rename** - Rename a symbol across all occurrences.
> - **Smooth Scrolling** - Eased scroll animation.
> - **Indent Guides** - Scope-highlighted vertical indent lines.
> - **Brace Matching** - Highlighted pairs, red for unclosed.
> - **Smart Brackets** - Auto-pairing for `{}`, `()`, `[]`, `""`, `''`.
> - **Fullscreen Mode** - Expand editor to fill the game window.
> - **Shortcut Overlay** - View all keyboard shortcuts at a glance.
- **Java Scripting (Janino):**
> Write scripts in real Java compiled at runtime. Name methods after hooks for automatic wiring. Language selector per tab to switch between Java and JavaScript. External `.java` file support.
- **Client-Side Scripting:**
> Run scripts on the client for responsive experiences. Server-controlled -- off by default, must be explicitly enabled. Script files sync automatically on login and reload.
- **Cloner Tab Overhaul:**
> Folder-based organization for saved NPCs, items, and entries. Full-width directory browser for large collections. Improved tab navigation.
- **Addon API Support:**
> Addons can ship type definitions that load into the script editor automatically. Full autocomplete, hover info, and documentation for addon APIs. Addons can extend existing CNPC+ types with new methods.
- **Animation Improvements:**
> Data store for passing data across animation events and frames. New consumer-based task system for animation lifecycle. Expanded scripting API for animation control.
**Extras**
- Player Dialog Events now work like NPC Dialog Events.
- Attack speed configuration for linked items.
- Client-side balance prediction during Trader interactions.
- Right-click cycling on multi-option NPC buttons (Hair, Eyes, Fur, etc.).
- Fixed quest cooldown for MC Custom and RL Custom timer types.
- Fixed Bard music restarting when opening dialogs.
- Fixed NPC biome spawn settings not saving after editing.
- Fixed block waypoint issues.
- Fixed script config syncing between server and client.
+118
View File
@@ -0,0 +1,118 @@
# ✏️ CustomNPC+ 1.11 - The Scripting Update ✏️
---
The **Scripting Update** completely transforms how you write scripts in CNPC+! A brand new **Script Editor** with **syntax highlighting**, **autocomplete**, and **error detection** makes scripting feel like a real IDE. Write scripts in **Java** with the new Janino engine, enable **client-side scripting** for responsive experiences, and organize your Cloner with **folders**!
---
## Script Editor 📝
The script editor has been rebuilt from the ground up into a **full-featured code editor** right inside Minecraft.
### Writing Code
- **Syntax Highlighting** - Keywords, types, variables, methods, strings, and comments are all color-coded automatically. The editor understands your code -- local variables, global fields, and chained method calls each get their own color
- **Autocomplete** - Start typing and get smart suggestions for methods, fields, and classes. Suggestions know whether you're in a static or instance context, and frequently used members appear first
- **Auto Imports** - Type a class name and the editor suggests the correct import for you
- **Error Detection** - Catch mistakes before you even run your script! Wrong argument types, missing methods, type mismatches, unused imports, and more are all underlined with detailed error messages on hover
> Hover over any symbol to see its **full type information**, documentation, and declaration -- just like a desktop IDE.
---
### Navigation
- **Go To Line** - `CTRL+G` jumps to any line instantly
- **Search & Replace** - `CTRL+F` to search, `CTRL+H` to replace. All matches highlight in real-time
- **Go To Definition** - `CTRL+Click` any method, field, or type to jump straight to its definition
- **Fullscreen Mode** - Expand the editor to fill your entire game window for maximum workspace
### Editing
- **Undo/Redo** - Smart undo that works on whole words at a time, not individual characters
- **Line Operations** - Duplicate lines with `CTRL+D`, delete lines, and move lines up or down with shortcuts
- **Comment Toggle** - `CTRL+/` to comment or uncomment selected lines
- **Rename** - Rename a variable or method and every occurrence updates at once
- **Copy Whole Line** - Copy with nothing selected grabs the entire line
- **Triple Click** - Select an entire line instantly
### Visual Polish
- **Line Numbers** - A clean gutter with line numbers and current line highlighting
- **Smooth Scrolling** - Eased scroll animations for a polished feel
- **Indent Guides** - Vertical lines show your code's nesting depth, with the current scope highlighted in green
- **Brace Matching** - Matching `{}` braces light up, and unclosed braces turn red
- **Smart Brackets** - Typing `{` automatically creates the closing `}` with correct indentation. Same for `()`, `[]`, `""`, and `''`
- **Keyboard Shortcut Overlay** - Press a button to see all available shortcuts at a glance
---
## Java Scripting ☕
Write scripts in **real Java** instead of JavaScript! Powered by the **Janino** compiler, your Java code is compiled and runs natively.
- **Full Java Language** - Use classes, interfaces, enums, generics, lambdas, and everything you know from Java
- **Automatic Hook Resolution** - Just name your methods after the hook you want (e.g., `init`, `tick`, `interact`) and they're wired up automatically
- **Language Selector** - Each script tab has a dropdown to choose between JavaScript and Java. Mix and match across tabs
- **External Files** - Write `.java` scripts in an external editor and they're loaded automatically
> Java and JavaScript scripts coexist side by side. Pick whichever language fits your workflow!
---
## Client-Side Scripting 🖥️
Scripts can now run **on the client** for responsive, visual experiences!
- **Server Controlled** - Server owners decide whether client scripting is allowed. Players can't enable it on their own
- **Automatic Sync** - Script files are sent from the server to all connected clients on login, and re-synced whenever scripts are reloaded
- **Safe by Default** - Client scripting is off by default and must be explicitly enabled in the server config
> Client-side scripts open the door to custom UI effects, visual feedback, and client-only hooks -- all while the server stays in full control.
---
## Cloner Tab Overhaul 📂
The Cloner now supports **folders** for organizing your saved NPCs, items, and entries!
- **Folder System** - Create folders and subfolders to keep your cloner organized however you like
- **Full Screen Browser** - A new full-width directory view makes browsing large collections much easier
- **Improved Tabs** - Better tab navigation for switching between cloner categories
---
## Addon API Support 📘
Addon developers can now ship **type definitions** alongside their mods, giving scripters full autocomplete and documentation for addon APIs!
- **Automatic Loading** - Any mod can include API definitions and they'll appear in the script editor automatically
- **Patch Support** - Addons can extend existing CNPC+ types with new methods (e.g., a DBC addon adding `getDBCPlayer()` to the Player type)
- **Full Documentation** - Parameter names, return types, and descriptions all show up in autocomplete and hover info
> If you're a scripter using addons, you get autocomplete for their APIs with zero setup. If you're an addon developer, just include your definitions and everything works.
---
## Animation Improvements 🎬
- **Data Store** - Animations can now store and pass data across events and frames for more complex animation logic
- **Task System** - A new consumer-based system for reacting to animation lifecycle events, replacing the old approach
- **Expanded API** - More animation control exposed to the scripting API
---
## Additional Changes 🔧
- **Player Dialog Events** - Now work the same way as NPC Dialog Events for consistency
- **Linked Item Attack Speed** - Added attack speed configuration to linked items
- **Trader Balance Preview** - Client-side balance prediction shows your expected balance during trades
- **Right-Click Cycling** - Multi-option NPC buttons (Hair, Eyes, Fur, etc.) can now be right-clicked to cycle backwards
- **Quest Cooldown Fix** - Fixed cooldown not working for MC Custom and RL Custom timer types
- **Bard Music Fix** - Fixed music restarting or duplicating when opening dialogs
- **Biome Spawn Fix** - Fixed NPC biome spawn settings not saving after editing
- **Block Waypoint Fix** - Fixed issues with block waypoints
- **Script Config Fix** - Fixed script configuration not syncing properly between server and client
---
+25
View File
@@ -3,6 +3,7 @@
plugins {
id 'com.gtnewhorizons.gtnhconvention'
id 'idea'
id "dts.typescript-generator"
}
version = "1.11-prerelease3"
@@ -56,6 +57,30 @@ sourceSets.main.java {
srcDirs += apiDir
}
// ============================================================================
// TypeScript Definition Generation Task
// Generates .d.ts files from Java API sources for scripting IDE support
// ============================================================================
// TypeScript plugin is applied above in the main plugins block
tasks.named("generateTypeScriptDefinitions").configure {
sourceDirectories = ['src/api/java', 'src/main/java']
outputDirectory = "src/main/resources/assets/${project.property("modId")}/api"
apiPackages = ['noppes.npcs.api', 'net.minecraft'] as Set
cleanOutputFirst = true // Clean old generated files before regenerating
patchesDirectory = "dts-patches"
implementationPackages = [
'noppes.npcs.scripted',
'noppes.npcs.controllers.data',
'noppes.npcs.entity.data',
'noppes.npcs.quests',
'kamkeel.npcs.controllers.data'
] as Set
}
processResources.dependsOn generateTypeScriptDefinitions
sourcesJar.dependsOn generateTypeScriptDefinitions
// Register @hookName as a valid Javadoc tag
tasks.withType(Javadoc) {
options.tags = [
+2
View File
@@ -45,6 +45,8 @@ dependencies {
shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar"))
annotationProcessor(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar"))
testImplementation("junit:junit:4.13.2")
if(embedMixin){
// Normal Jar will include built in Mixins
+126
View File
@@ -0,0 +1,126 @@
/**
* ECMAScript 5.1 - Array type for working with ordered collections of elements.
* Supports static methods like Array.isArray() and instance methods like push(), map(), filter().
*
* ## How to Use
*
* ### Creating Arrays
* ```js
* var empty = []; // Empty array
* var nums = [1, 2, 3]; // Array with values
* var mixed = ["hello", 42, true]; // Mixed types
* var nested = [[1, 2], [3, 4]]; // Nested arrays
* ```
*
* ### Static Methods
* ```js
* Array.isArray([1, 2, 3]); // true
* Array.isArray("hello"); // false
* Array.isArray({length: 3}); // false
* ```
*
* ### Adding & Removing Elements
* ```js
* var arr = [1, 2, 3];
* arr.push(4); // [1, 2, 3, 4] — add to end
* arr.pop(); // [1, 2, 3] — remove from end
* arr.unshift(0); // [0, 1, 2, 3] — add to start
* arr.shift(); // [1, 2, 3] — remove from start
* arr.splice(1, 1, 99); // [1, 99, 3] — replace at index 1
* ```
*
* ### Searching & Iterating
* ```js
* var arr = [10, 20, 30, 20];
* arr.indexOf(20); // 1 (first match)
* arr.lastIndexOf(20); // 3 (last match)
* arr.forEach(function(val) { log(val); });
* var doubled = arr.map(function(v) { return v * 2; }); // [20, 40, 60, 40]
* var big = arr.filter(function(v) { return v > 15; }); // [20, 30, 20]
* ```
*
* ### Reducing & Testing
* ```js
* var sum = [1, 2, 3].reduce(function(acc, v) { return acc + v; }, 0); // 6
* var allPositive = [1, 2, 3].every(function(v) { return v > 0; }); // true
* var hasNeg = [1, -2, 3].some(function(v) { return v < 0; }); // true
* ```
*
* ### ❌ Common Mistakes
* ```js
* Array.push(1); // TypeError — push is an instance method
* var a = [3,1,2]; a.sort(); // Sorts as strings! Use compareFn for numbers
* a.sort(function(a, b) { return a - b; }); // Correct numeric sort: [1, 2, 3]
* ```
*/
export interface Array<T> {
/** Returns true if the given object is an array, false otherwise */
static isArray(arg: any): boolean;
/** The number of elements in the array */
length: number;
/** Combines two or more arrays and returns a new array without modifying the original */
concat(...items: T[]): T[];
/** Tests whether all elements in the array pass the provided test function */
every(callback: Java.java.util.function.Function<T, boolean>): boolean;
/** Creates a new array with all elements that pass the test implemented by the provided function */
filter(callback: Java.java.util.function.Function<T, boolean>): T[];
/** Executes a provided function once for each array element. Returns undefined */
forEach(callback: Java.java.util.function.Consumer<T>): void;
/** Returns the first index at which an element is found, or -1 if not present */
indexOf(searchElement: T): number;
indexOf(searchElement: T, fromIndex: number): number;
/** Joins all elements of an array into a string separated by the given separator */
join(separator: string): string;
/** Returns the last index at which an element is found, or -1 if not present */
lastIndexOf(searchElement: T): number;
lastIndexOf(searchElement: T, fromIndex: number): number;
/** Creates a new array with the results of calling a function on every element */
map(callback: Java.java.util.function.Function<T, any>): any[];
/** Removes and returns the last element from the array, modifying the original array */
pop(): T;
/** Adds one or more elements to the end of an array and returns the new length */
push(...items: T[]): number;
/** Applies a function against an accumulator and each element to reduce the array to a single value */
reduce(callback: Java.java.util.function.BiFunction<any, T, any>): any;
reduce(callback: Java.java.util.function.BiFunction<any, T, any>, initialValue: any): any;
/** Same as reduce() but processes the array from right to left */
reduceRight(callback: Java.java.util.function.BiFunction<any, T, any>): any;
reduceRight(callback: Java.java.util.function.BiFunction<any, T, any>, initialValue: any): any;
/** Reverses an array in place. The first element becomes the last, and vice versa */
reverse(): T[];
/** Removes and returns the first element from the array, modifying the original array */
shift(): T;
/** Returns a shallow copy of a portion of an array as a new array object */
slice(start: number, end: number): T[];
/** Tests whether at least one element in the array passes the test implemented by the provided function */
some(callback: Java.java.util.function.Function<T, boolean>): boolean;
/** Sorts the elements of an array in place and returns the sorted array */
sort(compareFn: Java.java.util.function.BiFunction<T, T, number>): T[];
/** Changes the contents of an array by removing or replacing existing elements and/or adding new ones */
splice(start: number, deleteCount: number, ...items: T[]): T[];
/** Returns a string representation of the array */
toString(): string;
/** Adds one or more elements to the beginning of an array and returns the new length */
unshift(...items: T[]): number;
}
+36
View File
@@ -0,0 +1,36 @@
/**
* ECMAScript 5.1 - Boolean type for true/false values.
* Booleans are the result of comparison and logical operations.
*
* ## How to Use
*
* ### Creating Booleans
* ```js
* var yes = true; // Boolean literal
* var no = false; // Boolean literal
* var fromExpr = (5 > 3); // true (comparison result)
* ```
*
* ### Boolean Methods
* ```js
* var b = true;
* b.toString(); // "true"
* b.valueOf(); // true (primitive value)
* ```
*
* ### Truthy & Falsy Values
* ```js
* // These are FALSY (evaluate to false):
* // false, 0, "", null, undefined, NaN
*
* // These are TRUTHY (evaluate to true):
* // true, any non-zero number, any non-empty string, objects, arrays
*
*/
export interface Boolean {
/** Returns the string "true" or "false" */
toString(): string;
/** Returns the primitive boolean value */
valueOf(): boolean;
}
+168
View File
@@ -0,0 +1,168 @@
/**
* ECMAScript 5.1 - Date type for working with dates and times.
* Create with `new Date()` or use static methods like Date.now() to get current timestamp.
*
* ## How to Use
*
* ### Creating Dates
* ```js
* var now = new Date(); // Current date & time
* var epoch = new Date(0); // Jan 1, 1970
* var specific = new Date(2024, 0, 15); // Jan 15, 2024 (months are 0-based!)
* var full = new Date(2024, 5, 15, 10, 30, 0); // Jun 15, 2024 10:30:00
* var fromStr = new Date("2024-01-15"); // Parse from ISO string
* ```
*
* ### Static Methods (no `new` needed)
* ```js
* var timestamp = Date.now(); // Current time in milliseconds
* var parsed = Date.parse("2024-01-15"); // Parse string to timestamp
* var utc = Date.UTC(2024, 0, 15); // UTC timestamp for Jan 15, 2024
* ```
*
* ### Common Mistakes
* ```js
* Date.now; // Returns the function, not the timestamp!
* Date.getMinutes(); // TypeError — instance method, not static
* new Date(2024, 1, 1); // Feb 1, not Jan 1 — months are 0-based!
* ```
*/
export interface Date {
/** Returns the current time as the number of milliseconds since 1970-01-01 00:00:00 UTC */
static now(): number;
/** Parses a date string and returns the number of milliseconds since 1970-01-01 00:00:00 UTC */
static parse(s: string): number;
/** Returns the number of milliseconds since 1970-01-01 00:00:00 UTC for the given UTC date/time components */
static UTC(year: number, month: number, date: number, hours: number, minutes: number, seconds: number, ms: number): number;
/** Returns the day of the month (1-31) in local time */
getDate(): number;
/** Returns the day of the week (0=Sunday, 6=Saturday) in local time */
getDay(): number;
/** Returns the year in local time */
getFullYear(): number;
/** Returns the hour (0-23) in local time */
getHours(): number;
/** Returns the milliseconds (0-999) in local time */
getMilliseconds(): number;
/** Returns the minutes (0-59) in local time */
getMinutes(): number;
/** Returns the month (0=January, 11=December) in local time */
getMonth(): number;
/** Returns the seconds (0-59) in local time */
getSeconds(): number;
/** Returns the number of milliseconds since 1970-01-01 00:00:00 UTC */
getTime(): number;
/** Returns the time zone offset in minutes between local time and UTC */
getTimezoneOffset(): number;
/** Returns the day of the month (1-31) in UTC */
getUTCDate(): number;
/** Returns the day of the week (0=Sunday, 6=Saturday) in UTC */
getUTCDay(): number;
/** Returns the year in UTC */
getUTCFullYear(): number;
/** Returns the hour (0-23) in UTC */
getUTCHours(): number;
/** Returns the milliseconds (0-999) in UTC */
getUTCMilliseconds(): number;
/** Returns the minutes (0-59) in UTC */
getUTCMinutes(): number;
/** Returns the month (0=January, 11=December) in UTC */
getUTCMonth(): number;
/** Returns the seconds (0-59) in UTC */
getUTCSeconds(): number;
/** Sets the day of the month and returns the new timestamp */
setDate(date: number): number;
/** Sets the year, and optionally month and date, then returns the new timestamp */
setFullYear(year: number, month: number, date: number): number;
/** Sets the hour, minutes, seconds, and milliseconds, then returns the new timestamp */
setHours(hour: number, min: number, sec: number, ms: number): number;
/** Sets the milliseconds and returns the new timestamp */
setMilliseconds(ms: number): number;
/** Sets the minutes, seconds, and milliseconds, then returns the new timestamp */
setMinutes(min: number, sec: number, ms: number): number;
/** Sets the month, and optionally date, then returns the new timestamp */
setMonth(month: number, date: number): number;
/** Sets the seconds and milliseconds, then returns the new timestamp */
setSeconds(sec: number, ms: number): number;
/** Sets the timestamp (milliseconds since epoch) and returns it */
setTime(time: number): number;
/** Sets the day of the month (UTC) and returns the new timestamp */
setUTCDate(date: number): number;
/** Sets the year, month, and date (UTC), then returns the new timestamp */
setUTCFullYear(year: number, month: number, date: number): number;
/** Sets the hour, minutes, seconds, and milliseconds (UTC), then returns the new timestamp */
setUTCHours(hour: number, min: number, sec: number, ms: number): number;
/** Sets the milliseconds (UTC) and returns the new timestamp */
setUTCMilliseconds(ms: number): number;
/** Sets the minutes, seconds, and milliseconds (UTC), then returns the new timestamp */
setUTCMinutes(min: number, sec: number, ms: number): number;
/** Sets the month and date (UTC), then returns the new timestamp */
setUTCMonth(month: number, date: number): number;
/** Sets the seconds and milliseconds (UTC), then returns the new timestamp */
setUTCSeconds(sec: number, ms: number): number;
/** Returns a string representation of the date portion in local time (e.g., "Mon Jan 01 2024") */
toDateString(): string;
/** Returns the date as an ISO 8601 string (e.g., "2024-01-01T00:00:00.000Z") */
toISOString(): string;
/** Returns a JSON representation of the date (typically an ISO 8601 string) */
toJSON(key: any): string;
/** Returns a localized string for the date portion (format depends on locale) */
toLocaleDateString(): string;
/** Returns a localized string for the entire date and time (format depends on locale) */
toLocaleString(): string;
/** Returns a localized string for the time portion (format depends on locale) */
toLocaleTimeString(): string;
/** Returns a string representation of the date and time in local time */
toString(): string;
/** Returns a string representation of the time portion in local time (e.g., "00:00:00 GMT+0000") */
toTimeString(): string;
/** Returns a string representation of the date and time in UTC (e.g., "Mon, 01 Jan 2024 00:00:00 GMT") */
toUTCString(): string;
/** Returns the primitive value of the date object (milliseconds since 1970-01-01 00:00:00 UTC) */
valueOf(): number;
}
+81
View File
@@ -0,0 +1,81 @@
/**
* ECMAScript 5.1 - Error base type for runtime errors.
* Throw Error objects to indicate failure conditions in your code.
*
* ## How to Use
*
* ### Creating & Throwing Errors
* ```js
* throw new Error("Something went wrong");
* throw new TypeError("Expected a string");
* throw new RangeError("Value out of range");
* ```
*
* ### Catching Errors
* ```js
* try {
* var result = riskyOperation();
* } catch (e) {
* log(e.name); // "Error", "TypeError", etc.
* log(e.message); // "Something went wrong"
* log(e.toString()); // "Error: Something went wrong"
* }
* ```
*
* ### Custom Error Messages
* ```js
* function divide(a, b) {
* if (b === 0) {
* throw new Error("Cannot divide by zero");
* }
* return a / b;
* }
* ```
*
* ### ❌ Common Mistakes
* ```js
* throw "Something went wrong"; // Works but no .name or .message properties
* Error("oops"); // Creates error but doesn't throw it!
* throw Error("oops"); // ✅ Works (with or without `new`)
* ```
*/
export interface Error {
/** The name of the error type (e.g., "Error", "TypeError", "RangeError") */
name: string;
/** A human-readable description of what went wrong */
message: string;
/** Returns a string representation of the error (typically "name: message") */
toString(): string;
}
/**
* EvalError - Thrown when eval() function is misused (rarely used in modern JavaScript)
*/
export interface EvalError extends Error {}
/**
* RangeError - Thrown when a numeric value is out of the acceptable range (e.g., array length < 0)
*/
export interface RangeError extends Error {}
/**
* ReferenceError - Thrown when trying to reference a variable that does not exist or is not in scope
*/
export interface ReferenceError extends Error {}
/**
* SyntaxError - Thrown when parsing invalid syntax (usually caught during compile/parse, not runtime)
*/
export interface SyntaxError extends Error {}
/**
* TypeError - Thrown when an operation is performed on a value of the wrong type (e.g., calling a non-function)
*/
export interface TypeError extends Error {}
/**
* URIError - Thrown by URI encoding/decoding functions when given invalid characters or sequences
*/
export interface URIError extends Error {}
+75
View File
@@ -0,0 +1,75 @@
/**
* ECMAScript 5.1 - Global functions available in the global scope.
* These are not on any specific object - call them directly (e.g., parseInt("42")).
*
* ## How to Use
*
* ### Parsing Numbers
* ```js
* parseInt("42"); // 42
* parseInt("42px"); // 42 (stops at non-digit)
* parseInt("0xFF", 16); // 255 (hexadecimal)
* parseInt("111", 2); // 7 (binary)
* parseFloat("3.14"); // 3.14
* parseFloat("3.14abc"); // 3.14 (stops at invalid char)
* ```
*
* ### Checking Special Values
* ```js
* isNaN(NaN); // true
* isNaN(42); // false
* isNaN("hello"); // true (cannot parse as number)
* isNaN("42"); // false (can parse as number)
* isFinite(42); // true
* isFinite(Infinity); // false
* isFinite(NaN); // false
* ```
*
* ### URI Encoding/Decoding
* ```js
* encodeURI("hello world"); // "hello%20world"
* decodeURI("hello%20world"); // "hello world"
* encodeURIComponent("a=1&b=2"); // "a%3D1%26b%3D2" (encodes & and =)
* decodeURIComponent("a%3D1%26b%3D2"); // "a=1&b=2"
* ```
*
* ### ❌ Common Mistakes
* ```js
* parseInt("08"); // Some engines treat as octal! Always specify radix
* parseInt("08", 10); // 8 ✅ — explicitly base-10
* parseInt(""); // NaN — empty string returns NaN, not 0
* parseFloat(""); // NaN
* isNaN(undefined); // true — coerces to NaN first
* ```
*/
export interface GlobalFunctions {
/** Evaluates JavaScript code in a string (use with caution - slow and potential security issues) */
static eval(x: any): any;
/** Parses a string and returns an integer. Stops parsing at the first non-digit character */
static parseInt(string: string): number;
/** Parses a string and returns an integer using the specified radix (base). Stops parsing at the first non-digit character */
static parseInt(string: string, radix: number): number;
/** Parses a string and returns a floating-point number. Stops parsing at the first invalid character */
static parseFloat(string: string): number;
/** Determines whether a value is NaN (not-a-number). The value is first coerced to a number before testing */
static isNaN(value: any): boolean;
/** Determines whether a value is a finite number. The value is first coerced to a number. Returns false for Infinity, -Infinity, and NaN */
static isFinite(value: any): boolean;
/** Decodes a URI by replacing encoded sequences like %20 with their original characters */
static decodeURI(encodedURI: string): string;
/** Decodes URI component by replacing ALL encoded sequences including those for reserved URI characters */
static decodeURIComponent(encodedURIComponent: string): string;
/** Encodes a URI by replacing special characters with %HH sequences (but preserves reserved characters like / : ?) */
static encodeURI(uri: string): string;
/** Encodes URI component by replacing ALL special characters including reserved ones with %HH sequences */
static encodeURIComponent(uriComponent: string): string;
}
+100
View File
@@ -0,0 +1,100 @@
/**
* ECMAScript 5.1 - JSON built-in object for parsing and serializing JSON data.
*
* ## How to Use
*
* JSON is a **static object** — use its methods directly, never with `new`.
*
* ### Parsing JSON Strings to Objects
* ```js
* var obj = JSON.parse('{"name": "Steve", "level": 5}');
* log(obj.name); // "Steve"
* log(obj.level); // 5
*
* var arr = JSON.parse('[1, 2, 3]');
* log(arr[0]); // 1
* ```
*
* ### Converting Objects to JSON Strings
* ```js
* var data = { name: "Steve", hp: 100 };
* var json = JSON.stringify(data); // '{"name":"Steve","hp":100}'
*
* // Pretty-printed with 2-space indent:
* var pretty = JSON.stringify(data, null, 2);
* // '{
* // "name": "Steve",
* // "hp": 100
* // }'
* ```
*
* ### Storing & Loading Data
* ```js
* // Save to NPC stored data:
* var inventory = { sword: 1, potion: 5 };
* npc.getStoredData().put("inv", JSON.stringify(inventory));
*
* // Load from NPC stored data:
* var raw = npc.getStoredData().get("inv");
* var loaded = JSON.parse(raw);
* log(loaded.potion); // 5
* ```
*
* ### ❌ Common Mistakes
* ```js
* new JSON(); // TypeError — JSON is not a constructor
* JSON.parse('{name: "Steve"}'); // SyntaxError — keys must be quoted
* JSON.parse("{'name': 'Steve'}"); // SyntaxError — must use double quotes
* JSON.parse('{"name": "Steve"}'); // ✅ Correct — double-quoted keys & values
* ```
*/
export interface JSON {
/**
* Parses a JSON string and returns the parsed value (object, array, string, number, boolean, or null).
* @param text - A valid JSON string to parse
* @returns The parsed JavaScript value
*/
static parse(text: string): any;
/**
* Parses a JSON string with a reviver function that transforms each parsed key-value pair.
* @param text - A valid JSON string to parse
* @param reviver - Function called for each key-value pair; receives the key and parsed value, returns the transformed value
* @returns The parsed JavaScript value after applying the reviver
*/
static parse(text: string, reviver: Java.java.util.function.BiFunction<string, any, any>): any;
/**
* Converts a JavaScript value to a JSON string.
* @param value - The value to serialize (object, array, string, number, boolean, or null)
* @returns A JSON-formatted string representation of the value
*/
static stringify(value: any): string;
/**
* Converts a JavaScript value to a JSON string, using a replacer function to filter or transform properties.
* @param value - The value to serialize
* @param replacer - Function called for each key-value pair; receives the key and value, returns the value to include (or undefined to omit)
* @param space - Optional indentation: a number of spaces (up to 10), or a string prefix per level
* @returns A JSON-formatted string representation of the value
*/
static stringify(value: any, replacer: Java.java.util.function.BiFunction<string, any, any>, space?: any): string;
/**
* Converts a JavaScript value to a JSON string, including only the specified properties.
* @param value - The value to serialize
* @param replacer - Array of property names to include in the output
* @param space - Optional indentation: a number of spaces (up to 10), or a string prefix per level
* @returns A JSON-formatted string representation of the value
*/
static stringify(value: any, replacer: string[], space?: any): string;
/**
* Converts a JavaScript value to a JSON string with optional pretty-printing.
* @param value - The value to serialize
* @param replacer - Pass null to include all properties
* @param space - Indentation: a number of spaces (up to 10), or a string prefix per level
* @returns A JSON-formatted string representation of the value
*/
static stringify(value: any, replacer: null, space: any): string;
}
+128
View File
@@ -0,0 +1,128 @@
/**
* ECMAScript 5.1 - Math built-in object providing mathematical constants and functions.
* All methods are static and called as Math.methodName(). No constructor.
*
* ## How to Use
*
* Math is a **static object** — use its methods directly, never with `new`.
*
* ### Basic Math Operations
* ```js
* var rounded = Math.floor(3.7); // 3
* var ceiling = Math.ceil(3.2); // 4
* var nearest = Math.round(3.5); // 4
* var absolute = Math.abs(-42); // 42
* ```
*
* ### Powers, Roots & Logarithms
* ```js
* var squared = Math.pow(2, 3); // 8
* var root = Math.sqrt(16); // 4
* var natural = Math.log(Math.E); // 1
* ```
*
* ### Random Numbers
* ```js
* var rand = Math.random(); // 0.0 to 0.999...
* // Random integer between 1 and 10:
* var dice = Math.floor(Math.random() * 10) + 1;
* ```
*
* ### Finding Min/Max
* ```js
* var biggest = Math.max(5, 10, 3); // 10
* var smallest = Math.min(5, 10, 3); // 3
* ```
*
* ### Trigonometry
* ```js
* var sine = Math.sin(Math.PI / 2); // 1
* var cosine = Math.cos(0); // 1
* var angle = Math.atan2(1, 1); // 0.785... (PI/4)
* ```
*
* ### ❌ Common Mistakes
* ```js
* new Math() // TypeError — Math is not a constructor
* Math() // TypeError — Math is not a function
* ```
*/
export interface Math {
/** Euler's number (~2.718), the base of natural logarithms */
static readonly E: number;
/** Natural logarithm of 2 (~0.693) */
static readonly LN2: number;
/** Natural logarithm of 10 (~2.303) */
static readonly LN10: number;
/** Base-2 logarithm of e (~1.443) */
static readonly LOG2E: number;
/** Base-10 logarithm of e (~0.434) */
static readonly LOG10E: number;
/** Ratio of a circle's circumference to its diameter (~3.14159) */
static readonly PI: number;
/** Square root of 0.5 (~0.707) */
static readonly SQRT1_2: number;
/** Square root of 2 (~1.414) */
static readonly SQRT2: number;
/** Returns the absolute value of a number */
static abs(x: number): number;
/** Returns the arccosine (in radians) of a number */
static acos(x: number): number;
/** Returns the arcsine (in radians) of a number */
static asin(x: number): number;
/** Returns the arctangent (in radians) of a number */
static atan(x: number): number;
/** Returns the arctangent of two numbers (y/x), with proper quadrant handling */
static atan2(y: number, x: number): number;
/** Returns the smallest integer greater than or equal to a number */
static ceil(x: number): number;
/** Returns the cosine (in radians) of a number */
static cos(x: number): number;
/** Returns e raised to the power of a number */
static exp(x: number): number;
/** Returns the largest integer less than or equal to a number */
static floor(x: number): number;
/** Returns the natural logarithm (base e) of a number */
static log(x: number): number;
/** Returns the largest of zero or more numbers */
static max(...values: number[]): number;
/** Returns the smallest of zero or more numbers */
static min(...values: number[]): number;
/** Returns base raised to the power of exponent */
static pow(x: number, y: number): number;
/** Returns a random number between 0 (inclusive) and 1 (exclusive) */
static random(): number;
/** Returns the value of a number rounded to the nearest integer */
static round(x: number): number;
/** Returns the sine (in radians) of a number */
static sin(x: number): number;
/** Returns the square root of a number */
static sqrt(x: number): number;
/** Returns the tangent (in radians) of a number */
static tan(x: number): number;
}
+74
View File
@@ -0,0 +1,74 @@
/**
* ECMAScript 5.1 - Number type for working with numeric values and constants.
* Access static constants like Number.MAX_VALUE or format numbers with instance methods.
*
* ## How to Use
*
* ### Number Constants
* ```js
* Number.MAX_VALUE; // ~1.79e+308 (largest representable number)
* Number.MIN_VALUE; // ~5e-324 (smallest positive number)
* Number.NaN; // NaN
* Number.POSITIVE_INFINITY; // Infinity
* Number.NEGATIVE_INFINITY; // -Infinity
* ```
*
* ### Formatting Numbers
* ```js
* var n = 3.14159;
* n.toFixed(2); // "3.14" (fixed decimal places)
* n.toFixed(0); // "3"
* n.toPrecision(4); // "3.142" (significant digits)
* n.toExponential(2); // "3.14e+0" (scientific notation)
* ```
*
* ### Converting to String
* ```js
* var n = 255;
* n.toString(); // "255" (base 10, default)
* n.toString(16); // "ff" (hexadecimal)
* n.toString(2); // "11111111" (binary)
* n.toString(8); // "377" (octal)
* ```
*
* ### ❌ Common Mistakes
* ```js
* Number.toFixed(2); // TypeError — instance method, not static
* (3).toFixed(2); // "3.00" ✅ — wrap literal in parentheses
* 3.toFixed(2); // SyntaxError — parser reads 3. as float
* ```
*/
export interface Number {
/** The largest representable number (~1.79e+308) */
static readonly MAX_VALUE: number;
/** The smallest representable positive number (~5e-324) */
static readonly MIN_VALUE: number;
/** Represents "Not a Number" (NaN) - the result of undefined or invalid arithmetic operations */
static readonly NaN: number;
/** Represents negative infinity, the result of overflow or division by zero */
static readonly NEGATIVE_INFINITY: number;
/** Represents positive infinity, the result of overflow or division by zero */
static readonly POSITIVE_INFINITY: number;
/** Formats the number using exponential notation with optional fractional digits */
toExponential(fractionDigits: number): string;
/** Formats the number as a fixed-point string with the specified number of fractional digits (0-20) */
toFixed(fractionDigits: number): string;
/** Returns a localized string representation of the number (format depends on locale) */
toLocaleString(): string;
/** Formats the number as a string with the specified number of significant digits (1-21) */
toPrecision(precision: number): string;
/** Converts the number to a string in the specified radix (base 2-36, default 10) */
toString(radix: number): string;
/** Returns the primitive numeric value of this Number object */
valueOf(): number;
}
+105
View File
@@ -0,0 +1,105 @@
/**
* ECMAScript 5.1 - Object type, the base type for all JavaScript objects.
* Use static methods like Object.keys() and Object.create() to manipulate object structure.
*
* ## How to Use
*
* ### Creating Objects
* ```js
* var obj = {}; // Empty object literal
* var person = { name: "Steve", age: 20 }; // Object with properties
* var child = Object.create(person); // Create with prototype
* ```
*
* ### Inspecting Objects
* ```js
* var obj = { a: 1, b: 2, c: 3 };
* Object.keys(obj); // ["a", "b", "c"]
* Object.getOwnPropertyNames(obj); // ["a", "b", "c"] (includes non-enumerable)
* obj.hasOwnProperty("a"); // true
* obj.hasOwnProperty("toString"); // false (inherited, not own)
* ```
*
* ### Freezing & Sealing
* ```js
* var config = { debug: false };
* Object.freeze(config); // Cannot modify, add, or delete properties
* config.debug = true; // Silently fails (or throws in strict mode)
* Object.isFrozen(config); // true
*
* var settings = { volume: 50 };
* Object.seal(settings); // Can modify values, but cannot add/delete
* settings.volume = 75; // ✅ Works
* settings.newProp = true; // Silently fails
* ```
*
* ### Property Descriptors
* ```js
* var obj = {};
* Object.defineProperty(obj, "id", {
* value: 42,
* writable: false, // Cannot be changed
* enumerable: true // Shows up in for-in / Object.keys
* });
* ```
*
* ### ❌ Common Mistakes
* ```js
* Object.keys(); // TypeError — needs an argument
* obj.keys(); // TypeError — keys is a static method
* Object.hasOwnProperty("a"); // Wrong — instance method on the object
* ```
*/
export interface Object {
/** Creates a new object with the specified prototype and optional property descriptors */
static create(proto: any, propertiesObject: any): any;
/** Defines or modifies multiple properties on an object and returns the modified object */
static defineProperties(obj: any, props: any): any;
/** Defines a new property on an object or modifies an existing property and returns the object */
static defineProperty(obj: any, prop: string, descriptor: any): any;
/** Prevents all modifications to an object (cannot add, modify, or delete properties) */
static freeze(obj: any): any;
/** Returns a property descriptor for a named property on an object, or undefined if not found */
static getOwnPropertyDescriptor(obj: any, prop: string): any;
/** Returns an array of all property names (both enumerable and non-enumerable) on an object */
static getOwnPropertyNames(obj: any): string[];
/** Returns the prototype object of the specified object */
static getPrototypeOf(obj: any): any;
/** Determines whether an object is extensible (can have new properties added to it) */
static isExtensible(obj: any): boolean;
/** Determines whether an object has been frozen (is immutable) */
static isFrozen(obj: any): boolean;
/** Determines whether an object is sealed (cannot add or delete properties) */
static isSealed(obj: any): boolean;
/** Returns an array of all enumerable property names on an object */
static keys(obj: any): string[];
/** Prevents new properties from being added to an object but allows modification of existing ones */
static preventExtensions(obj: any): any;
/** Prevents all modifications to an object except for reading and writing property values */
static seal(obj: any): any;
/** Determines whether an object has a property with the specified name (direct property, not inherited) */
hasOwnProperty(prop: string): boolean;
/** Determines whether an object is in the prototype chain of another object */
isPrototypeOf(obj: any): boolean;
/** Determines whether an object property is enumerable (would be included in a for-in loop) */
propertyIsEnumerable(prop: string): boolean;
/** Returns a localized string representation of the object (format depends on locale) */
toLocaleString(): string;
}
+94
View File
@@ -0,0 +1,94 @@
/**
* ECMAScript 5.1 - RegExp type for pattern matching and string manipulation.
* Create with the RegExp constructor or literal syntax: /pattern/flags
*
* ## How to Use
*
* ### Creating Regular Expressions
* ```js
* // Method 1: Regex literal (preferred)
* var re = /hello/;
* var reFlags = /hello/gi; // g=global, i=ignore case, m=multiline
*
* // Method 2: Constructor (useful for dynamic patterns)
* var re2 = new RegExp("hello");
* var re3 = new RegExp("hello", "gi");
* var dynamic = new RegExp(playerName); // Build pattern from variable
* ```
*
* ### Testing for Matches
* ```js
* /\d+/.test("abc123"); // true — contains digits
* /^\d+$/.test("abc123"); // false — not ALL digits
* /^\d+$/.test("12345"); // true — all digits
* /hello/i.test("Hello World"); // true — case insensitive
* ```
*
* ### Extracting Matches
* ```js
* var match = /\d+/.exec("abc123def"); // ["123"]
* var noMatch = /\d+/.exec("no digits"); // null
*
* // With capturing groups
* var result = /(\w+)@(\w+)/.exec("user@domain");
* // result[0] = "user@domain" (full match)
* // result[1] = "user" (first group)
* // result[2] = "domain" (second group)
* ```
*
* ### Using with String Methods
* ```js
* "abc123".match(/\d+/); // ["123"]
* "abc123".search(/\d/); // 3 (index of first digit)
* "abc123".replace(/\d+/, "NUM"); // "abcNUMdef"
* "a,b,,c".split(/,+/); // ["a", "b", "c"]
* ```
*
* ### RegExp Properties
* ```js
* var re = /hello/gi;
* re.source; // "hello"
* re.global; // true
* re.ignoreCase; // true
* re.multiline; // false
* re.lastIndex; // 0 (changes with global exec/test)
* ```
*
* ### ❌ Common Mistakes
* ```js
* RegExp.test("string"); // TypeError — test is an INSTANCE method!
* new RegExp.test("string"); // TypeError — incorrect syntax
* // ✅ CORRECT:
* /pattern/.test("string"); // Use on a regex instance
* new RegExp("pattern").test("string"); // Or create instance first
*
* // Double-escape backslashes in constructor strings:
* new RegExp("\d+"); // Wrong — \d becomes just d
* new RegExp("\\d+"); // ✅ Correct — \\d becomes \d
* ```
*/
export interface RegExp {
/** The pattern string that was used to create this regular expression */
source: string;
/** True if the 'g' (global) flag is set, affecting match behavior in exec() and test() */
global: boolean;
/** True if the 'i' (ignore case) flag is set, making pattern matching case-insensitive */
ignoreCase: boolean;
/** True if the 'm' (multiline) flag is set, affecting ^ and $ anchor behavior */
multiline: boolean;
/** The index at which to start searching in the string (used with 'g' flag in exec() loops) */
lastIndex: number;
/** Searches the string for a match and returns an array of matches or null if no match found */
exec(string: string): any[];
/** Tests whether the pattern matches the string, returning true or false */
test(string: string): boolean;
/** Returns a string representation of the regular expression (e.g., "/pattern/flags") */
toString(): string;
}
+82
View File
@@ -0,0 +1,82 @@
/**
* ECMAScript 5.1 - String type for working with text. Strings are immutable sequences of characters.
* Use String.fromCharCode() to create a string from character codes.
*
* ## How to Use
*
* ### Creating Strings
* ```js
* var greeting = "Hello, world!"; // String literal (double quotes)
* var name = 'Steve'; // String literal (single quotes)
* var fromCode = String.fromCharCode(65); // "A" (from char code)
* ```
*
* ### Searching & Checking
* ```js
* var str = "Hello, world!";
* str.indexOf("world"); // 7
* str.indexOf("missing"); // -1 (not found)
* str.lastIndexOf("l"); // 10
* str.search(/world/); // 7 (regex search)
* str.charAt(0); // "H"
* str.charCodeAt(0); // 72
* str.length; // 13
* ```
*
* ### Extracting & Transforming
* ```js
* var str = "Hello, world!";
* str.slice(7, 12); // "world"
* str.substring(0, 5); // "Hello"
* str.toUpperCase(); // "HELLO, WORLD!"
* str.toLowerCase(); // "hello, world!"
* str.trim(); // Removes leading/trailing whitespace
* str.replace("world", "NPC"); // "Hello, NPC!"
* str.replace(/l/g, "L"); // "HeLLo, worLd!" (regex with global flag)
* ```
*
* ### Splitting & Joining
* ```js
* "a,b,c".split(","); // ["a", "b", "c"]
* "hello".split(""); // ["h", "e", "l", "l", "o"]
* ["a", "b", "c"].join("-"); // "a-b-c" (Array.join, but related)
* "one two three".split(" ", 2); // ["one", "two"] (limit results)
* ```
*
* ### Pattern Matching
* ```js
* "abc123".match(/\d+/); // ["123"]
* "no numbers".match(/\d+/); // null
* "hello".concat(" ", "world"); // "hello world"
* ```
*
* ### ❌ Common Mistakes
* ```js
* str.replace("l", "L"); // Only replaces FIRST "l" — use /l/g for all
* String.charAt(0); // TypeError — instance method, not static
* "abc"[0]; // May not work in all engines — use charAt()
* ```
*/
export interface String {
/** The number of characters in the string */
length: number;
/** Creates a string from one or more Unicode character codes (e.g., String.fromCharCode(65, 66, 67) returns "ABC") */
static fromCharCode(...codes: number[]): string;
/** Returns the Unicode code point (0–65535) of the character at the specified index. Unlike Java's charAt() + cast, this returns the numeric code directly. */
charCodeAt(pos: number): number;
/** Tests the string against a regular expression and returns an array of matches, or null if no match found */
match(regexp: any): any[];
/**
* Replaces a pattern (string or RegExp object) with a replacement string.
* Unlike Java's replace/replaceAll which only accept String/CharSequence,
* this accepts a RegExp object as the first argument.
*/
replace(searchValue: any, replaceValue: any): string;
/** Returns the index of the first character that matches a regular expression, or -1 if not found */
search(regexp: any): number;
}
+12 -1
View File
@@ -1,6 +1,14 @@
pluginManagement {
// resolutionStrategy {
// eachPlugin {
// if(requested.id.toString() == "dts.typescript-generator") {
// useModule("com.github.bigguy345:dts-gradle-plugin:95b8ab4")
// }
// }
// }
//
repositories {
maven { url "https://jitpack.io"}
maven {
// RetroFuturaGradle
name "GTNH Maven"
@@ -19,3 +27,6 @@ pluginManagement {
plugins {
id 'com.gtnewhorizons.gtnhsettingsconvention' version '1.0.27'
}
// Include the local gradle-plugins composite build so plugin is resolved locally
includeBuild 'gradle-plugins'
@@ -159,6 +159,8 @@ import noppes.npcs.client.gui.roles.GuiNpcTransporter;
import noppes.npcs.client.gui.script.GuiScriptGlobal;
import noppes.npcs.client.gui.script.GuiScriptInterface;
import noppes.npcs.client.gui.util.script.PackageFinder;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
import noppes.npcs.client.gui.util.script.interpreter.type.ClassIndex;
import noppes.npcs.client.model.ModelNPCGolem;
import noppes.npcs.client.model.ModelNpcCrystal;
import noppes.npcs.client.model.ModelNpcDragon;
@@ -863,6 +865,8 @@ public class ClientProxy extends CommonProxy {
public void buildPackageIndex() {
try {
PackageFinder.init(Thread.currentThread().getContextClassLoader());
ClassIndex.init();
JSTypeRegistry.getInstance().initializeFromResources();
} catch (IOException e) {
}
}
@@ -9,6 +9,7 @@ import net.minecraft.util.ResourceLocation;
import net.minecraft.util.StatCollector;
import noppes.npcs.CustomNpcs;
import noppes.npcs.LogWriter;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
import javax.imageio.ImageIO;
import java.awt.Graphics;
@@ -30,6 +31,11 @@ public class CustomNpcResourceListener implements
FolderResourcePack pack = new FolderResourcePack(CustomNpcs.Dir);
simplemanager.reloadResourcePack(pack);
// Reload scripting .d.ts files
JSTypeRegistry registry = JSTypeRegistry.getInstance();
registry.clear();
registry.initializeFromResources();
try {
DefaultTextColor = Integer.parseInt(StatCollector.translateToLocal("customnpcs.defaultTextColor"), 16);
@@ -16,6 +16,8 @@ import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextArea;
import noppes.npcs.client.gui.util.GuiScriptTextArea;
import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer;
import noppes.npcs.constants.EnumScriptType;
import noppes.npcs.constants.ScriptContext;
import noppes.npcs.controllers.ScriptContainer;
import noppes.npcs.controllers.ScriptController;
@@ -88,18 +90,25 @@ public class GuiScript extends GuiScriptInterface {
super.initGui();
this.guiTop += 10;
// ==================== TOP BUTTONS (hidden in fullscreen) ====================
if (!isFullscreen) {
GuiMenuTopButton top;
addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts"));
addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts"));
addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X"));
top.active = showScript;
addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings"));
top.active = !showScript;
addTopButton(new GuiMenuTopButton(15, top, "gui.website"));
if (isFullscreen) {
FullscreenConfig.paddingTop = 30;
}
// ==================== TOP BUTTONS ====================
boolean isFullscreenView = isFullscreen && showScript;
int menuX = isFullscreenView ? FullscreenConfig.paddingLeft : guiLeft + 4;
int menuY = isFullscreenView ? FullscreenConfig.paddingTop - 20 : guiTop - 17;
int rightX = isFullscreenView ? width - FullscreenConfig.paddingRight : guiLeft + xSize;
GuiMenuTopButton top;
addTopButton(top = new GuiMenuTopButton(13, menuX, menuY, "script.scripts"));
addTopButton(new GuiMenuTopButton(16, rightX - 102, menuY, "eventscript.eventScripts"));
addTopButton(new GuiMenuTopButton(17, rightX - 22, menuY, "X"));
top.active = showScript;
addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings"));
top.active = !showScript;
addTopButton(new GuiMenuTopButton(15, top, "gui.website"));
if (showScript) {
initScriptView();
} else {
@@ -162,6 +171,10 @@ public class GuiScript extends GuiScriptInterface {
String language = (container != null) ? container.getLanguage() : script.getLanguage();
activeArea.setLanguage(language);
activeArea.setScriptContext(getScriptContext());
// Set editor globals based on the active NPC hook
String hookName = EnumScriptType.values()[activeTab].function;
applyEditorGlobals(activeArea, hookName);
// Setup fullscreen key binding
GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> {
@@ -244,6 +257,19 @@ public class GuiScript extends GuiScriptInterface {
}
}
// Apply editor globals for the active NPC hook.
private void applyEditorGlobals(GuiScriptTextArea activeArea, String hookName) {
if (activeArea == null)
return;
ScriptTextContainer textContainer = activeArea.getContainer();
if (textContainer == null)
return;
if (script != null)
textContainer.setEditorGlobalsMap(script.getEditorGlobals(hookName));
}
@Override
protected String getConsoleText() {
Map<Long, String> map = this.script.getOldConsoleText();
@@ -382,24 +408,4 @@ public class GuiScript extends GuiScriptInterface {
initGui();
}
}
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
super.drawScreen(mouseX, mouseY, partialTicks);
// Draw fullscreen button when in script view (GuiScript uses 0-based activeTab)
if (showScript) {
fullscreenButton.draw(mouseX, mouseY);
}
}
@Override
public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
// Check fullscreen button first when in script view
if (showScript && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) {
return;
}
super.mouseClicked(mouseX, mouseY, mouseButton);
}
}
@@ -25,6 +25,7 @@ import noppes.npcs.client.gui.util.IGuiData;
import noppes.npcs.client.gui.util.IJTextAreaListener;
import noppes.npcs.client.gui.util.ITextChangeListener;
import noppes.npcs.client.gui.util.ITextfieldListener;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
import noppes.npcs.constants.ScriptContext;
import noppes.npcs.controllers.ScriptContainer;
import noppes.npcs.controllers.ScriptController;
@@ -32,15 +33,11 @@ import noppes.npcs.controllers.data.ForgeDataScript;
import noppes.npcs.controllers.data.IScriptHandler;
import noppes.npcs.controllers.data.IScriptHandlerPacket;
import noppes.npcs.controllers.data.IScriptUnit;
import noppes.npcs.janino.JaninoScript;
import noppes.npcs.scripted.item.ScriptCustomItem;
import org.lwjgl.opengl.Display;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener {
@@ -105,6 +102,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
this.drawDefaultBackground = true;
this.closeOnEsc = true;
this.xSize = 420;
this.isPannableGUI = true;
this.setBackground("menubg.png");
}
@@ -120,6 +118,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
// Initialize hooks from handler
gui.hookList = new ArrayList<>(handler.getHooks());
JSTypeRegistry registry = JSTypeRegistry.getInstance();
if (!registry.isInitialized()) {
registry.initializeFromResources();
}
registry.syncHooksFromScriptHookControllerIfNeeded();
// Request data from server
if (handler instanceof IScriptHandlerPacket)
((IScriptHandlerPacket) handler).requestData();
@@ -139,6 +143,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
public void initGui() {
// ==================== BASE LAYOUT CALCULATION ====================
isPanning = false;
this.ySize = (int) ((double) this.xSize * 0.56D);
if ((double) this.ySize > (double) this.height * 0.95D) {
this.ySize = (int) ((double) this.height * 0.95D);
@@ -278,6 +283,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
// Set the script context for context-aware hook autocomplete
activeArea.setScriptContext(getScriptContext());
activeArea.enableCodeHighlighting();
updateScriptDocumentImports(activeArea, container);
// Setup fullscreen key binding
GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> {
@@ -328,8 +336,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
}
this.addScroll(scroll);
}
}
computePanBounds(editorX, editorY, editorWidth, editorHeight);
}
/**
* Initialize the settings tab layout (console view).
*/
@@ -371,6 +380,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
// Language button requires options to be available
this.getButton(103).enabled = languageOptions.size() > 0;
}
int consoleX = this.guiLeft + 4 + yoffset;
int consoleY = this.guiTop + 6 + yoffset;
int consoleW = this.xSize - 160 - yoffset;
int consoleH = (int) ((float) this.ySize * 0.92F) - yoffset * 2;
computePanBounds(consoleX, consoleY, consoleW, consoleH);
}
public GuiScriptInterface setDimensions(int x, int y) {
@@ -389,30 +404,34 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
return handler.getContext();
}
// ==================== RENDERING ====================
@Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) {
super.drawScreen(mouseX, mouseY, partialTicks);
// Draw fullscreen button on top of everything when on script editor tab
// Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves
if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer()) {
fullscreenButton.draw(mouseX, mouseY);
protected boolean isPannableArea(int mx, int my) {
if (activeTab > 0 && fullscreenButton.isMouseOver(mx, my)) {
return false;
}
GuiScriptTextArea activeArea = getActiveScriptArea();
if (activeArea != null && activeArea.isPointOnAutocompleteMenu(mx, my)) {
return false;
}
return super.isPannableArea(mx, my);
}
// ==================== MOUSE HANDLING ====================
@Override
public void mouseClicked(int mouseX, int mouseY, int mouseButton) {
// Check fullscreen button first when on script editor tab
// Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves
if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer() && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) {
public void mouseClicked(int i, int j, int k) {
int adjX = panAdjustedX(i);
int adjY = panAdjustedY(j);
GuiScriptTextArea activeArea = getActiveScriptArea();
boolean isOverAutocomplete = activeArea != null
&& activeArea.isPointOnAutocompleteMenu(adjX, adjY);
if (isOverAutocomplete) {
activeArea.mouseClicked(adjX, adjY, k);
return;
}
super.mouseClicked(mouseX, mouseY, mouseButton);
super.mouseClicked(i, j, k);
}
public String previousHookClicked = "";
@@ -435,11 +454,40 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
this.getTextField(2).setText(this.getTextField(2).getText() + addString);
previousHookClicked = "";
updateScriptDocumentImports(getActiveScriptArea(), container);
} else {
previousHookClicked = hook;
}
}
public void updateScriptDocumentImports(GuiScriptTextArea activeArea, IScriptUnit container) {
// For JaninoScripts, add implicit imports (default imports + hook signature types)
// These allow the syntax highlighter to resolve types without explicit import statements
if (container instanceof JaninoScript) {
JaninoScript<?> janinoScript = (JaninoScript<?>) container;
ScriptContext ctx = getScriptContext();
// Add default imports (e.g., noppes.npcs.api.*, noppes.npcs.api.entity.*, etc.)
activeArea.addImplicitImports(janinoScript.getDefaultImports());
// Add hook types from signatures (parameters + return types)
// e.g., INpcEvent.InitEvent, Color, String, IOverlayContext, etc.
Set<String> hookTypes = janinoScript.getHookTypes();
activeArea.addImplicitImports(hookTypes.toArray(new String[0]));
// Add wildcard imports for each event class in the script context: INpcEvent.*, IProjectileEvent.*
// so nested types like INpcEvent.CollideEvent resolve without explicit import
Set<String> wildcardImports = new HashSet<>();
for (String fqn : ctx.getNamespaceFQNs())
wildcardImports.add(fqn + ".*");
activeArea.addImplicitImports(wildcardImports.toArray(new String[0]));
// Format to update which implicit imports are actually used
activeArea.formatCodeText();
}
}
@Override
public void unFocused(GuiNpcTextField textfield) {
@@ -61,6 +61,30 @@ public abstract class GuiNPCInterface extends GuiScreen {
public float bgScaleZ = 1;
public int bgTextureHeight = 256; // Actual content height in texture (for bottom border stitching)
// ==================== VIEWPORT PANNING ====================
/**
* If true, this GUI supports viewport panning via click-drag on empty areas.
* Subclasses set this to true and call computePanBounds() to enable.
*/
protected boolean isPannableGUI = false;
protected boolean isPanning = false;
protected double panStartX, panStartY;
/**
* Cumulative pan offset in scaled GUI pixels.
* Positive values = viewport shifted right/down (content appears to move left/up).
*/
public double currentPanX, currentPanY;
public double getPanX() { return currentPanX; }
public double getPanY() { return currentPanY; }
protected double minPanX, maxPanX, minPanY, maxPanY;
protected int panViewportX, panViewportY, panViewportWidth, panViewportHeight;
/** Pan-adjusted mouse coordinates, updated every frame in drawScreen for hit-testing panned components. */
protected int panAdjMouseX, panAdjMouseY;
public GuiNPCInterface(EntityNPCInterface npc) {
this.player = Minecraft.getMinecraft().thePlayer;
this.npc = npc;
@@ -161,28 +185,39 @@ public abstract class GuiNPCInterface extends GuiScreen {
if (subgui != null)
subgui.mouseClicked(i, j, k);
else {
for (GuiNpcTextField tf : new ArrayList<GuiNpcTextField>(textfields.values()))
if (tf.enabled)
tf.mouseClicked(i, j, k);
int adjX = isPannableGUI ? panAdjustedX(i) : i;
int adjY = isPannableGUI ? panAdjustedY(j) : j;
if (isPannableGUI && k == 0 && isPannableArea(adjX, adjY)) {
isPanning = true;
panStartX = i;
panStartY = j;
return;
}
mouseEvent(adjX, adjY, k);
vanillaMouseClicked(adjX, adjY, k);
for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) {
guiScrollableComponent.mouseClicked(i, j, k);
guiScrollableComponent.mouseClicked(adjX, adjY, k);
}
if (k == 0) {
for (GuiCustomScroll scroll : new ArrayList<GuiCustomScroll>(scrolls.values())) {
scroll.mouseClicked(i, j, k);
scroll.mouseClicked(adjX, adjY, k);
}
}
// Process diagram mouse clicks
for (GuiDiagram diagram : diagrams.values()) {
if (diagram.isWithin(i, j)) {
if (diagram.mouseClicked(i, j, k))
if (diagram.isWithin(adjX, adjY)) {
if (diagram.mouseClicked(adjX, adjY, k))
return;
}
}
mouseEvent(i, j, k);
vanillaMouseClicked(i, j, k);
for (GuiNpcTextField tf : new ArrayList<GuiNpcTextField>(textfields.values()))
if (tf.enabled)
tf.mouseClicked(adjX, adjY, k);
}
}
@@ -223,7 +258,12 @@ public abstract class GuiNPCInterface extends GuiScreen {
subgui.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
return;
}
super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick);
if (isPannableGUI && isPanning) {
return;
}
int adjX = isPannableGUI ? panAdjustedX(mouseX) : mouseX;
int adjY = isPannableGUI ? panAdjustedY(mouseY) : mouseY;
super.mouseClickMove(adjX, adjY, clickedMouseButton, timeSinceLastClick);
}
@Override
@@ -232,12 +272,72 @@ public abstract class GuiNPCInterface extends GuiScreen {
subgui.mouseMovedOrUp(mouseX, mouseY, state);
return;
}
super.mouseMovedOrUp(mouseX, mouseY, state);
if (isPannableGUI && state == 0 && isPanning) {
isPanning = false;
panStartX = 0;
panStartY = 0;
return;
}
int adjX = isPannableGUI ? panAdjustedX(mouseX) : mouseX;
int adjY = isPannableGUI ? panAdjustedY(mouseY) : mouseY;
super.mouseMovedOrUp(adjX, adjY, state);
}
public void mouseEvent(int i, int j, int k) {
}
// ==================== VIEWPORT PANNING METHODS ====================
protected boolean isPannableArea(int mx, int my) {
for (Object obj : this.buttonList) {
GuiButton btn = (GuiButton) obj;
if (btn.visible && mx >= btn.xPosition && mx < btn.xPosition + btn.width
&& my >= btn.yPosition && my < btn.yPosition + btn.height) {
return false;
}
}
for (GuiCustomScroll scroll : scrolls.values()) {
if (scroll.visible && scroll.isMouseOver(mx, my)) {
return false;
}
}
for (GuiNpcTextField tf : textfields.values()) {
if (tf.enabled && mx >= tf.xPosition && mx < tf.xPosition + tf.width
&& my >= tf.yPosition && my < tf.yPosition + tf.height) {
return false;
}
}
return true;
}
protected void computePanBounds(int vpX, int vpY, int vpW, int vpH) {
this.panViewportX = vpX;
this.panViewportY = vpY;
this.panViewportWidth = vpW;
this.panViewportHeight = vpH;
int panRange = Math.max(this.width, this.height) / 2;
this.minPanX = -panRange;
this.maxPanX = panRange;
this.minPanY = -panRange;
this.maxPanY = panRange;
}
protected int panAdjustedX(int rawMouseX) {
return rawMouseX + (int) currentPanX;
}
protected int panAdjustedY(int rawMouseY) {
return rawMouseY + (int) currentPanY;
}
public boolean hasPanOffset() {
return isPannableGUI && (currentPanX != 0 || currentPanY != 0);
}
@Override
protected void actionPerformed(GuiButton guibutton) {
if (subgui != null)
@@ -375,9 +475,41 @@ public abstract class GuiNPCInterface extends GuiScreen {
public void drawScreen(int i, int j, float f) {
mouseX = i;
mouseY = j;
boolean panning = hasPanOffset();
int drawX = panning ? panAdjustedX(i) : i;
int drawY = panning ? panAdjustedY(j) : j;
panAdjMouseX = drawX;
panAdjMouseY = drawY;
// Smooth pan: update pan offset every frame using raw mouse delta
if (isPannableGUI && isPanning && Mouse.isButtonDown(0)) {
double deltaX = i - panStartX;
double deltaY = j - panStartY;
currentPanX = Math.max(minPanX, Math.min(maxPanX, currentPanX - deltaX));
currentPanY = Math.max(minPanY, Math.min(maxPanY, currentPanY - deltaY));
panStartX = i;
panStartY = j;
panning = hasPanOffset();
drawX = panning ? panAdjustedX(i) : i;
drawY = panning ? panAdjustedY(j) : j;
panAdjMouseX = drawX;
panAdjMouseY = drawY;
} else if (isPanning && !Mouse.isButtonDown(0)) {
isPanning = false;
}
if (drawDefaultBackground && subgui == null)
drawDefaultBackground();
if (panning) {
GL11.glPushMatrix();
GL11.glTranslated(-currentPanX, -currentPanY, 0);
}
if (background != null && mc.renderEngine != null) {
drawBackground();
}
@@ -386,45 +518,45 @@ public abstract class GuiNPCInterface extends GuiScreen {
drawCenteredString(fontRendererObj, title, width / 2, guiTop + 4, 0xffffff);
for (GuiNpcLabel label : labels.values())
label.drawLabel(this, fontRendererObj);
for (GuiNpcTextField tf : textfields.values()) {
tf.drawTextBox(i, j);
}
for (GuiCustomScroll scroll : scrolls.values()) {
scroll.updateSubGUI(subGui);
scroll.drawScreen(i, j, f, !subGui && scroll.isMouseOver(i, j) ? Mouse.getDWheel() : 0);
scroll.drawScreen(drawX, drawY, f, !subGui && scroll.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0);
}
// Draw scrollable windows.
for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) {
guiScrollableComponent.drawScreen(i, j, f, !subGui && guiScrollableComponent.isMouseOver(i, j) ? Mouse.getDWheel() : 0);
guiScrollableComponent.drawScreen(drawX, drawY, f, !subGui && guiScrollableComponent.isMouseOver(drawX, drawY) ? Mouse.getDWheel() : 0);
}
for (GuiDiagram diagram : diagrams.values()) {
diagram.drawDiagram(i, j, subGui);
diagram.drawDiagram(drawX, drawY, subGui);
}
super.drawScreen(i, j, f);
super.drawScreen(drawX, drawY, f);
for (GuiCustomScroll scroll : scrolls.values())
if (scroll.hoverableText) {
scroll.drawHover(i, j);
scroll.drawHover(drawX, drawY);
}
for (GuiNpcButton button : buttons.values()) {
button.updateSubGUI(subGui);
if (!button.hoverableText.isEmpty()) {
button.drawHover(i, j, subGui);
button.drawHover(drawX, drawY, subGui);
}
}
for (GuiNpcTextField textField : textfields.values()) {
if (textField.hasHoverText()) {
textField.drawHover(i, j, subGui);
}
for (GuiNpcTextField tf : textfields.values()) {
tf.drawTextBox(drawX, drawY);
if (tf.hasHoverText())
tf.drawHover(drawX, drawY, subGui);
}
for(GuiScrollWindow guiScrollableComponent : scrollWindows.values()) {
if (guiScrollableComponent.isMouseOver(mouseX, mouseY)) {
guiScrollableComponent.drawHoverTexts(mouseX, mouseY);
if (guiScrollableComponent.isMouseOver(drawX, drawY)) {
guiScrollableComponent.drawHoverTexts(drawX, drawY);
}
}
for (GuiScreen gui : extra.values())
gui.drawScreen(i, j, f);
gui.drawScreen(drawX, drawY, f);
if (panning) {
GL11.glPopMatrix();
}
if (subgui != null) {
subgui.drawScreen(i, j, f);
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -10,11 +10,19 @@ import org.lwjgl.opengl.GL11;
public class GuiUtil {
public static void setScissorClip(int x, int y, int width, int height) {
setScissorClip(x, y, width, height, null);
}
public static void setScissorClip(int x, int y, int width, int height, GuiNPCInterface gui) {
Minecraft mc = Minecraft.getMinecraft();
ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int scale = res.getScaleFactor();
if (gui != null) {
x -= (int) gui.getPanX();
y -= (int) gui.getPanY();
}
// Adjust position to Top-Left origin (OpenGL window/screen space uses bottom-left origin)
y = mc.displayHeight - y * scale;
height *= scale;
@@ -159,6 +167,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);
@@ -375,7 +375,8 @@ public class ClassPathFinder {
if (p == null || p.isEmpty()) continue;
String[] segs = p.split("\\.");
String last = segs[segs.length - 1];
boolean lastIsClass = last.length() > 0 && Character.isUpperCase(last.charAt(0));
// Check if last segment is actually a class (try to resolve it)
boolean lastIsClass = last.length() > 0 && tryResolveClass(p) != null;
// Try package wildcard: pkg.SimpleName
if (!lastIsClass) {
@@ -539,6 +540,8 @@ public class ClassPathFinder {
private int findFirstUppercaseSegment(String[] segments) {
for (int i = 0; i < segments.length; i++) {
if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) {
// Keep uppercase check here as it's a heuristic for package splitting
// This is about Java naming conventions, not actual type resolution
return i;
}
}
@@ -653,10 +656,10 @@ public class ClassPathFinder {
}
String typeName = content.substring(start, i);
// Only process if it looks like a type name (starts with uppercase)
if (Character.isUpperCase(typeName.charAt(0))) {
// Resolve the type
ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages);
// Try to resolve as an actual type
ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages);
if (info != null) {
// Only process if it actually resolves to a type
ClassType classType = (info != null) ? info.type : ClassType.CLASS;
results.add(new TypeOccurrence(baseOffset + start, baseOffset + i, typeName, classType));
File diff suppressed because it is too large. Load diff
@@ -8,6 +8,7 @@ import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Deprecated
public class JavaTextContainer extends TextContainer {
public static final Pattern MODIFIER = Pattern.compile(
@@ -2,6 +2,12 @@ package noppes.npcs.client.gui.util.script;
import net.minecraft.client.gui.GuiScreen;
import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData;
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
import noppes.npcs.client.gui.util.script.interpreter.ScriptTextContainer;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import org.lwjgl.input.Keyboard;
import java.util.ArrayList;
@@ -11,7 +17,7 @@ import java.util.regex.Pattern;
/**
* Handles IntelliJ-like rename refactoring with scope-aware renaming.
* <p>
*
* Features:
* - Scope detection: local variables rename only within their block, global fields rename everywhere
* - Visual feedback: primary occurrence has white border, others just highlighted
@@ -32,7 +38,11 @@ public class RenameRefactorHandler {
private int primaryOccurrenceEnd = -1;
private List<int[]> allOccurrences = new ArrayList<>();
private ScopeInfo scope = null;
// For parameter renaming: track containing method to find JSDoc
private boolean isParameterRename = false;
private MethodInfo containingMethod = null;
// For global scope, track positions that have local shadowing (to exclude them)
private List<int[]> localShadowedPositions = new ArrayList<>();
@@ -82,32 +92,11 @@ public class RenameRefactorHandler {
void scrollToPosition(int pos);
JavaTextContainer getContainer();
ScriptTextContainer getContainer();
int getViewportWidth(); // For determining if clicks are in rename box
}
/**
* Scope information for a variable
*/
public static class ScopeInfo {
public final int startOffset;
public final int endOffset;
public final boolean isGlobal;
public final String scopeType; // "global", "method", "block"
public ScopeInfo(int start, int end, boolean isGlobal, String type) {
this.startOffset = start;
this.endOffset = end;
this.isGlobal = isGlobal;
this.scopeType = type;
}
public boolean containsPosition(int pos) {
return pos >= startOffset && pos < endOffset;
}
}
public void setCallback(RenameCallback callback) {
this.callback = callback;
}
@@ -140,29 +129,62 @@ public class RenameRefactorHandler {
initialWord = text.substring(wordBounds[0], wordBounds[1]); // Remember for undo
originalWord = initialWord;
currentWord = initialWord;
// Determine scope for this identifier - MUST check if local shadows global
scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer());
// Track if this is a parameter rename by checking if position is in any method's parameter declaration
isParameterRename = false;
containingMethod = null;
ScriptTextContainer container = callback.getContainer();
if (container != null && container.getDocument() != null) {
ScriptDocument document = container.getDocument();
List<MethodInfo> methods = document.getAllMethods();
// Check if the word at cursor position is a parameter of any method
// A rename is a parameter rename if:
// 1. The word matches a parameter name
// 2. The cursor is within that method's declaration or body
for (MethodInfo method : methods) {
// Check if cursor is within this method's range
int methodStart = method.getFullDeclarationOffset();
int methodEnd = method.getBodyEnd();
if (methodStart < 0 || methodEnd < 0) continue; // External method
if (wordBounds[0] >= methodStart && wordBounds[0] <= methodEnd) {
// Check if the word matches any parameter of this method
for (FieldInfo param : method.getParameters()) {
if (param.getName().equals(originalWord)) {
isParameterRename = true;
containingMethod = method;
break;
}
}
if (isParameterRename) break;
}
}
}
// If global scope, find all positions where local variables shadow this name
localShadowedPositions.clear();
if (scope != null && scope.isGlobal) {
findLocalShadowedPositions(text, originalWord);
}
// Find all occurrences within scope
// Find all occurrences within scope (including JSDoc if it's a parameter)
findOccurrences(text, originalWord);
if (allOccurrences.isEmpty())
return false;
active = true;
// Set selection to full word (uses GuiScriptTextArea's selection)
primaryOccurrenceStart = wordBounds[0];
primaryOccurrenceEnd = wordBounds[1];
callback.getSelectionState().setSelection(wordBounds[0], wordBounds[1]);
// callback.setCursorPosition(wordBounds[1]);
// callback.setCursorPosition(wordBounds[1]);
callback.getSelectionState().markActivity();
return true;
@@ -206,7 +228,7 @@ public class RenameRefactorHandler {
// During live editing we used setTextWithoutUndo to avoid creating intermediate
// undo entries; pushing the original snapshot here gives a single predictable
// undo step back to the pre-rename state.
if (originalText != null && !originalText.isEmpty())
if (originalText != null && !originalText.isEmpty())
callback.pushUndoState(originalText, originalCursorPos);
// Calculate cursor position relative to primary occurrence
@@ -230,13 +252,14 @@ public class RenameRefactorHandler {
allOccurrences.clear();
localShadowedPositions.clear();
scope = null;
isParameterRename = false;
containingMethod = null;
originalText = "";
originalCursorPos = 0;
}
/**
* Handle keyboard input during rename
*
* @return true if input was consumed
*/
public boolean keyTyped(char c, int keyCode) {
@@ -264,7 +287,7 @@ public class RenameRefactorHandler {
// Get selection state
SelectionState sel = callback.getSelectionState();
boolean hasSelection = sel.hasSelection();
// Calculate cursor position within the word
int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart;
cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length()));
@@ -315,7 +338,7 @@ public class RenameRefactorHandler {
sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart);
callback.setCursorPosition(primaryOccurrenceStart);
}
// Left arrow
if (keyCode == Keyboard.KEY_LEFT) {
if (cursorInWord > 0) {
@@ -354,7 +377,7 @@ public class RenameRefactorHandler {
// Ctrl+A - select all (select the whole word)
if (keyCode == Keyboard.KEY_A &&
Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) {
sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart + currentWord.length());
callback.setCursorPosition(primaryOccurrenceStart + currentWord.length());
sel.markActivity();
@@ -536,23 +559,86 @@ public class RenameRefactorHandler {
allOccurrences.add(new int[]{start, end});
}
}
// If this is a parameter rename, also look for JSDoc @param occurrences
if (isParameterRename && containingMethod != null) {
findJSDocParamOccurrences(text, word);
}
}
/**
* Find JSDoc @param occurrences for a parameter name.
* Uses the JSDocInfo stored in the MethodInfo to locate the JSDoc comment,
* then searches for the parameter name pattern in the current text.
*/
private void findJSDocParamOccurrences(String text, String paramName) {
if (containingMethod == null) return;
// Get the JSDocInfo from the method to verify it has a @param for our parameter
JSDocInfo jsDocInfo = containingMethod.getJSDocInfo();
if (jsDocInfo == null) return;
// Check that this parameter has a JSDoc @param tag (using initial word to find the tag definition)
JSDocParamTag paramTag = jsDocInfo.getParamTag(initialWord);
if (paramTag == null) return;
// Get the approximate JSDoc location from the original document
int originalJsDocStart = jsDocInfo.getStartOffset();
int originalJsDocEnd = jsDocInfo.getEndOffset();
if (originalJsDocStart < 0 || originalJsDocEnd < 0) return;
// Calculate how much positions have shifted due to renames before the JSDoc
// Count occurrences that are currently in the text before the original JSDoc start
int shiftBeforeJsDoc = 0;
int lengthDiff = paramName.length() - initialWord.length();
// The occurrences we've already found (code occurrences) - count how many are before the JSDoc
for (int[] occ : allOccurrences) {
// Map current position back to approximate original position
int approxOriginalPos = occ[0] - shiftBeforeJsDoc;
if (approxOriginalPos < originalJsDocStart) {
shiftBeforeJsDoc += lengthDiff;
}
}
// Now search for the @param pattern in the current text around where the JSDoc should be
int adjustedJsDocStart = originalJsDocStart + shiftBeforeJsDoc;
int adjustedJsDocEnd = originalJsDocEnd + shiftBeforeJsDoc;
// Clamp to text bounds
adjustedJsDocStart = Math.max(0, adjustedJsDocStart);
adjustedJsDocEnd = Math.min(text.length(), adjustedJsDocEnd + 50); // Add some buffer for expanded text
if (adjustedJsDocStart >= adjustedJsDocEnd) return;
String jsDocRegion = text.substring(adjustedJsDocStart, adjustedJsDocEnd);
// Search for @param {Type} paramName or @param paramName pattern
Pattern pattern = Pattern.compile("@param\\s+(?:\\{[^}]*\\}\\s+)?(" + Pattern.quote(paramName) + ")(?:\\s|$|-)");
Matcher m = pattern.matcher(jsDocRegion);
while (m.find()) {
int paramNameStart = adjustedJsDocStart + m.start(1);
int paramNameEnd = adjustedJsDocStart + m.end(1);
allOccurrences.add(new int[]{paramNameStart, paramNameEnd});
}
}
/**
* Find all positions where local variables with the same name shadow the global variable.
* This populates localShadowedPositions with ranges where local declarations exist.
*/
private void findLocalShadowedPositions(String text, String varName) {
localShadowedPositions.clear();
List<MethodBlock> methods = MethodBlock.collectMethodBlocks(text);
List<int[]> excluded = MethodBlock.getExcludedRanges(text);
for (MethodBlock method : methods) {
// Check if this method has a local variable or parameter with the same name
boolean hasLocalDecl = method.localVariables.contains(varName);
boolean hasParamDecl = isParameterInMethod(text, varName, method);
if (hasLocalDecl || hasParamDecl) {
// Find where the local/param scope starts
int scopeStart;
@@ -564,18 +650,18 @@ public class RenameRefactorHandler {
scopeStart = findLocalDeclarationPosition(text, varName, method);
if (scopeStart < 0) scopeStart = method.startOffset;
}
// Find all occurrences of varName within this method's shadowed range
Pattern pattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b");
Matcher m = pattern.matcher(text);
while (m.find()) {
int pos = m.start();
// Skip if in string or comment
if (isInExcludedRange(pos, excluded))
continue;
// Check if within the shadowed scope of this method
if (pos >= scopeStart && pos < method.endOffset) {
localShadowedPositions.add(new int[]{m.start(), m.end()});
@@ -584,7 +670,7 @@ public class RenameRefactorHandler {
}
}
}
/**
* Check if a position is within a locally shadowed range
*/
@@ -595,13 +681,13 @@ public class RenameRefactorHandler {
}
return false;
}
/**
* Check if varName is a parameter in the method
*/
private boolean isParameterInMethod(String text, String varName, MethodBlock method) {
String methodHeader = text.substring(method.startOffset,
Math.min(method.startOffset + 500, method.endOffset));
Math.min(method.startOffset + 500, method.endOffset));
int parenStart = methodHeader.indexOf('(');
int parenEnd = methodHeader.indexOf(')');
if (parenStart >= 0 && parenEnd > parenStart) {
@@ -687,7 +773,7 @@ public class RenameRefactorHandler {
// Cursor is inside a method
// Check if it's a method parameter first
String methodHeader = text.substring(containingMethod.startOffset,
Math.min(containingMethod.startOffset + 500, containingMethod.endOffset));
Math.min(containingMethod.startOffset + 500, containingMethod.endOffset));
int parenStart = methodHeader.indexOf('(');
int parenEnd = methodHeader.indexOf(')');
if (parenStart >= 0 && parenEnd > parenStart) {
@@ -861,9 +947,9 @@ public class RenameRefactorHandler {
public boolean isWordFullySelected() {
if (callback == null) return false;
SelectionState selection = callback.getSelectionState();
return selection.hasSelection() &&
selection.getStartSelection() == primaryOccurrenceStart &&
selection.getEndSelection() == primaryOccurrenceEnd;
return selection.hasSelection() &&
selection.getStartSelection() == primaryOccurrenceStart &&
selection.getEndSelection() == primaryOccurrenceEnd;
}
/**
@@ -884,7 +970,7 @@ public class RenameRefactorHandler {
public boolean handleClick(int clickPosInText) {
if (!active || callback == null)
return false;
return false;
}
@@ -0,0 +1,26 @@
package noppes.npcs.client.gui.util.script;
/**
* Scope information for a variable
*/
public class ScopeInfo {
public final int startOffset;
public final int endOffset;
public final boolean isGlobal;
public final String scopeType; // "global", "method", "block"
public ScopeInfo(int start, int end, boolean isGlobal, String type) {
this.startOffset = start;
this.endOffset = end;
this.isGlobal = isGlobal;
this.scopeType = type;
}
public boolean containsPosition(int pos) {
return pos >= startOffset && pos < endOffset;
}
public boolean isWithin(ScopeInfo other) {
return this.startOffset >= other.startOffset && this.endOffset <= other.endOffset;
}
}
@@ -21,12 +21,12 @@ public class ScrollState {
private int scrollbarDragOffset = 0;
// Whether currently dragging the scrollbar
private boolean clickScrolling = false;
// Animation parameters
private static final double TAU = 0.1; // Time constant (~55ms feels snappy)
private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close
private static final double MAX_DT = 0.05; // Max delta time for stability
private static final double TAU = 0.15; // Time constant (~55ms feels snappy)
private static final double SNAP_THRESHOLD = 0.1; // Snap to target when this close (increased to avoid microstutters)
private static final double MAX_DT = 0.1; // Max delta time for stability
/**
* Reset scroll state to initial values
*/
@@ -37,7 +37,7 @@ public class ScrollState {
scrolledLine = 0;
lastScrollTime = System.currentTimeMillis();
}
/**
* Initialize scroll position if not already initialized
*/
@@ -47,19 +47,18 @@ public class ScrollState {
lastScrollTime = System.currentTimeMillis();
}
}
/**
* Update scroll animation. Call once per frame.
*
* @param maxScroll Maximum valid scroll position
*/
public void update(int maxScroll) {
long nowMs = System.currentTimeMillis();
double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0);
lastScrollTime = nowMs;
double dist = targetScroll - scrollPos;
// Snap to target if close enough
if (Math.abs(dist) < SNAP_THRESHOLD) {
scrollPos = targetScroll;
@@ -70,21 +69,21 @@ public class ScrollState {
double prev = scrollPos;
scrollPos += dist * alpha;
scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6);
// Clamp overshoot
if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) {
scrollPos = targetScroll;
scrollVelocity = 0.0;
}
}
// Keep in bounds
clampToBounds(maxScroll);
// Update integer line for rendering
scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll));
}
/**
* Clamp scroll position to valid bounds
*/
@@ -95,37 +94,35 @@ public class ScrollState {
if (targetScroll > maxScroll) targetScroll = maxScroll;
scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll));
}
/**
* Apply mouse wheel scroll
*
* @param wheelDelta Positive = scroll up, negative = scroll down
* @param maxScroll Maximum valid scroll position
* @param maxScroll Maximum valid scroll position
*/
public void applyWheelScroll(int wheelDelta, int maxScroll) {
double sign = Math.copySign(1, wheelDelta);
targetScroll -= sign * 2.0; // 2 lines per wheel tick
clampToBounds(maxScroll);
}
/**
* Set target scroll directly (for scrollbar dragging)
*/
public void setTargetScroll(double target, int maxScroll) {
targetScroll = Math.max(0, Math.min(target, maxScroll));
}
/**
* Scroll to make a specific line visible
*
* @param lineIdx Line index to make visible
* @param lineIdx Line index to make visible
* @param visibleLines Number of visible lines in viewport
* @param maxScroll Maximum scroll position
* @param maxScroll Maximum scroll position
*/
public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) {
int firstVisible = scrolledLine;
int lastFullyVisible = scrolledLine + visibleLines;
if (lineIdx < firstVisible) {
// Line is above viewport - scroll up
targetScroll = lineIdx;
@@ -135,66 +132,41 @@ public class ScrollState {
}
// If line is visible, don't change scroll
}
// Getters
public double getScrollPos() {
return scrollPos;
}
public double getScrollPos() { return scrollPos; }
public double getTargetScroll() { return targetScroll; }
public double getTargetScroll() {
return targetScroll;
}
/**
* @return First visible line
**/
/** @return First visible line **/
public int getScrolledLine() {
return scrolledLine;
}
public double getScrollVelocity() {
return scrollVelocity;
}
public boolean isClickScrolling() {
return clickScrolling;
}
public int getScrollbarDragOffset() {
return scrollbarDragOffset;
}
public double getScrollVelocity() { return scrollVelocity; }
public boolean isClickScrolling() { return clickScrolling; }
public int getScrollbarDragOffset() { return scrollbarDragOffset; }
/**
* Get fractional offset for sub-pixel rendering
*/
public double getFractionalOffset() {
return scrollPos - scrolledLine;
}
// Setters for scrollbar interaction
public void setClickScrolling(boolean scrolling) {
this.clickScrolling = scrolling;
}
public void setScrollbarDragOffset(int offset) {
this.scrollbarDragOffset = offset;
}
public void setScrolledLine(int line) {
this.scrolledLine = line;
}
public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; }
public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; }
public void setScrolledLine(int line) { this.scrolledLine = line; }
/**
* Handle scrollbar dragging interaction. This encapsulates the logic that was
* previously in the GUI class for handling thumb clicks/drags.
*
* @param yMouse current mouse Y coordinate
* @param areaX GUI area X (unused but kept for parity)
* @param areaY GUI area Y (top of text area)
* @param areaHeight height of the scroll track area
* @param yMouse current mouse Y coordinate
* @param areaX GUI area X (unused but kept for parity)
* @param areaY GUI area Y (top of text area)
* @param areaHeight height of the scroll track area
* @param visibleLines number of visible lines in viewport
* @param linesCount total number of lines
* @param maxScroll maximum allowed scroll value
* @param linesCount total number of lines
* @param maxScroll maximum allowed scroll value
*/
public void handleClickScrolling(int yMouse, int areaX, int areaY, int areaHeight, int visibleLines, int linesCount, int maxScroll) {
// Keep dragging while mouse button is down
@@ -238,4 +210,4 @@ public class ScrollState {
int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1;
setScrollbarDragOffset(yMouse - thumbTop);
}
}
}
@@ -5,6 +5,7 @@ import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.Gui;
import net.minecraft.util.ChatAllowedCharacters;
import net.minecraft.util.ResourceLocation;
import noppes.npcs.client.gui.util.GuiNPCInterface;
import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData;
import noppes.npcs.client.gui.util.GuiUtil;
import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer;
@@ -35,6 +36,7 @@ public class SearchReplaceBar {
// ==================== DIMENSIONS ====================
private int x, y, width;
public GuiNPCInterface ownerGui;
private int barHeight = 24;
private int replaceBarHeight = 22;
private int textFieldWidth = 150;
@@ -538,7 +540,7 @@ public class SearchReplaceBar {
}
GL11.glEnable(GL11.GL_SCISSOR_TEST);
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight);
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight, ownerGui);
// Draw selection highlight
@@ -604,7 +606,7 @@ public class SearchReplaceBar {
}
GL11.glEnable(GL11.GL_SCISSOR_TEST);
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight);
GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight, ownerGui);
{
// Draw selection highlight
@@ -0,0 +1,830 @@
package noppes.npcs.client.gui.util.script.autocomplete;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticField;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticParameter;
/**
* Represents a single autocomplete suggestion.
* Unified representation for both Java and JavaScript completions.
*/
public class AutocompleteItem implements Comparable<AutocompleteItem> {
/**
* The kind of completion item.
*/
public enum Kind {
ENUM_CONSTANT(-1), // Enum constant value (highest priority for enums)
METHOD(0), // Method or function
FIELD(1), // Field or property
VARIABLE(2), // Local variable or parameter
CLASS(3), // Class or interface type
ENUM(4), // Enum type
KEYWORD(6), // Language keyword
SNIPPET(7); // Code snippet
private final int priority;
Kind(int priority) {
this.priority = priority;
}
public int getPriority() {
return priority;
}
}
private final String name; // Display name (e.g., "getPlayer" or "getPlayer(UUID id)" for methods)
private final String searchName; // Name to search against (just the identifier, e.g., "getPlayer")
private final String insertText; // Text to insert (e.g., "getPlayer()")
private final Kind kind; // Type of completion
private final String typeLabel; // Type label (e.g., "IPlayer", "void")
private final TypeInfo typeInfo; // TypeInfo for the typeLabel
private final String signature; // Full signature for methods
private final String documentation; // Documentation/description
private final Object sourceData; // Original source (MethodInfo, FieldInfo, etc.)
private final boolean deprecated; // Whether this item is deprecated
// Import tracking
private final boolean requiresImport; // Whether selecting this item requires adding an import
private final String importPath; // Full path for import (e.g., "net.minecraft.client.Minecraft")
// Inheritance tracking (for JS types)
private final int inheritanceDepth; // Depth in inheritance tree (0 = child, 1 = parent, 2 = grandparent, etc.)
// Match scoring
private int matchScore = 0; // How well this matches the query
private int[] matchIndices; // Indices of matched characters for highlighting
private AutocompleteItem(String name, String searchName, String insertText, Kind kind, String typeLabel,
TypeInfo typeLabelTypeInfo, String signature, String documentation, Object sourceData,
boolean deprecated, boolean requiresImport, String importPath, int inheritanceDepth) {
this.name = name;
this.searchName = searchName != null ? searchName : name; // Default to name if not provided
this.insertText = insertText;
this.kind = kind;
this.typeLabel = typeLabel;
this.typeInfo = typeLabelTypeInfo;
this.signature = signature;
this.documentation = documentation;
this.sourceData = sourceData;
this.deprecated = deprecated;
this.requiresImport = requiresImport;
this.importPath = importPath;
this.inheritanceDepth = inheritanceDepth;
}
// ==================== FACTORY METHODS ====================
/**
* Create from a Java MethodInfo.
*/
public static AutocompleteItem fromMethod(MethodInfo method) {
return fromMethod(method, false);
}
/**
* Create from a Java MethodInfo, optionally for method reference context.
* @param method The method info
* @param forMethodReference If true, insert text will NOT include parentheses
*/
public static AutocompleteItem fromMethod(MethodInfo method, boolean forMethodReference) {
String name = method.getName();
StringBuilder insertText = new StringBuilder(name);
// For method references, don't add () since it's ::methodName not ::methodName()
if (!forMethodReference) {
insertText.append("(");
// Add placeholders for parameters if any
if (method.getParameterCount() > 0) {
// Just add () - user will fill in params
}
insertText.append(")");
}
String returnType = method.getReturnType() != null ?
getName(method.getReturnType()) : "void";
String signature = buildMethodSignature(method);
// Build display name with parameters like "read(byte[] b)"
StringBuilder displayName = new StringBuilder(name);
displayName.append("(");
for (int i = 0; i < method.getParameterCount(); i++) {
if (i > 0) displayName.append(", ");
FieldInfo param = method.getParameters().get(i);
String paramType = param.getTypeInfo() != null ?
getName(param.getTypeInfo()) : "?";
displayName.append(paramType);
if (param.isVarArg()) displayName.append("...");
if (param.getName() != null && !param.getName().isEmpty()) {
displayName.append(" ").append(param.getName());
}
}
displayName.append(")");
return new AutocompleteItem(
displayName.toString(),
name, // Search against just the method name, not the params
insertText.toString(),
Kind.METHOD,
returnType,
method.getReturnType(), // TypeInfo for return type
signature,
method.getDocumentation(),
method,
false, // TODO: Check for @Deprecated annotation
false, // Methods don't require imports
null,
-1 // Java methods don't use inheritance depth sorting
);
}
/**
* Create from a Java FieldInfo.
*/
public static AutocompleteItem fromField(FieldInfo field) {
String typeLabel = field.getTypeInfo() != null ?
getName(field.getTypeInfo()) : "?";
Kind kind;
switch (field.getScope()) {
case PARAMETER:
kind = Kind.VARIABLE;
break;
case LOCAL:
kind = Kind.VARIABLE;
break;
case ENUM_CONSTANT:
kind = Kind.ENUM_CONSTANT;
break;
default:
kind = Kind.FIELD;
}
return new AutocompleteItem(
field.getName(),
field.getName(), // searchName same as display name for fields
field.getName(),
kind,
typeLabel,
field.getTypeInfo(), // TypeInfo for field type
typeLabel + " " + field.getName(),
null,
field,
false,
false, // Fields don't require imports
null,
-1// Java fields don't use inheritance depth sorting
);
}
/**
* Create from a Java TypeInfo.
*/
public static AutocompleteItem fromType(TypeInfo type) {
Kind kind;
switch (type.getKind()) {
case INTERFACE:
kind = Kind.CLASS;
break;
case ENUM:
kind = Kind.ENUM;
break;
default:
kind = Kind.CLASS;
}
String name = getName(type);
return new AutocompleteItem(
name,
name, // searchName same as display name for types
name,
kind,
type.getPackageName(),
type, // TypeInfo for package name
type.getFullName(),
null,
type,
false,
false, // Will be overridden for unimported types
null,
-1 // Java types don't use inheritance depth sorting
);
}
/**
* Create from a JavaScript JSMethodInfo.
*/
public static AutocompleteItem fromJSMethod(JSMethodInfo method) {
return fromJSMethod(method, null, 0, false);
}
/**
* Create from a JavaScript JSMethodInfo with inheritance depth.
* @param method The method info
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
*/
public static AutocompleteItem fromJSMethod(JSMethodInfo method, int inheritanceDepth) {
return fromJSMethod(method, null, inheritanceDepth, false);
}
/**
* Create from a JavaScript JSMethodInfo with type parameter resolution for display.
* @param method The method info
* @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP)
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
*/
public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contextType, int inheritanceDepth) {
return fromJSMethod(method, contextType, inheritanceDepth, false);
}
/**
* Create from a JavaScript JSMethodInfo with type parameter resolution for display.
* @param method The method info
* @param contextType The TypeInfo context for resolving type parameters (e.g., IPlayer to resolve T → EntityPlayerMP)
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
* @param forMethodReference If true, insert text will NOT include parentheses
*/
public static AutocompleteItem fromJSMethod(JSMethodInfo method, TypeInfo contextType, int inheritanceDepth, boolean forMethodReference) {
String name = method.getName();
StringBuilder insertText = new StringBuilder(name);
// For method references, don't add () since it's ::methodName not ::methodName()
if (!forMethodReference) {
insertText.append("(");
insertText.append(")");
}
// Build display name with resolved parameter types
StringBuilder displayName = new StringBuilder(name);
displayName.append("(");
for (int i = 0; i < method.getParameters().size(); i++) {
if (i > 0) displayName.append(", ");
JSMethodInfo.JSParameterInfo param = method.getParameters().get(i);
// Use resolved type with full generic display
TypeInfo paramType = param.getResolvedType(contextType);
String paramTypeName = paramType.isResolved() ? paramType.getDisplayName() : param.getType();
if (param.isVarArg()) paramTypeName = paramTypeName + "...";
displayName.append(paramTypeName).append(" ").append(param.getName());
}
displayName.append(")");
// Get return type using the new method with full generic display
TypeInfo returnTypeInfo = method.getResolvedReturnType(contextType);
String returnType = returnTypeInfo.isResolved() ? returnTypeInfo.getDisplayName() : method.getReturnType();
return new AutocompleteItem(
displayName.toString(),
name, // Search against just the method name
insertText.toString(),
Kind.METHOD,
returnType,
returnTypeInfo, // TypeInfo for return type
method.getSignature(),
method.getDocumentation(),
method,
false,
false,
null,
inheritanceDepth
);
}
/**
* Create from a JavaScript JSFieldInfo.
*/
public static AutocompleteItem fromJSField(JSFieldInfo field) {
return fromJSField(field, null, 0);
}
/**
* Create from a JavaScript JSFieldInfo with inheritance depth.
* @param field The field info
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
*/
public static AutocompleteItem fromJSField(JSFieldInfo field, int inheritanceDepth) {
return fromJSField(field, null, inheritanceDepth);
}
/**
* Create from a JavaScript JSFieldInfo with type parameter resolution for display.
* @param field The field info
* @param contextType The TypeInfo context for resolving type parameters
* @param inheritanceDepth Depth in inheritance tree (0 = child, 1 = parent, etc.)
*/
public static AutocompleteItem fromJSField(JSFieldInfo field, TypeInfo contextType, int inheritanceDepth) {
// Get field type using the new method with full generic display
TypeInfo fieldTypeInfo = field.getResolvedType(contextType);
String fieldType = fieldTypeInfo.isResolved() ? fieldTypeInfo.getDisplayName() : field.getType();
return new AutocompleteItem(
field.getName(),
field.getName(), // searchName same as display name
field.getName(),
Kind.FIELD,
fieldType,
fieldTypeInfo, // TypeInfo for field type
field.toString(),
field.getDocumentation(),
field,
false,
false,
null,
inheritanceDepth
);
}
/**
* Create a keyword item.
*/
public static AutocompleteItem keyword(String keyword) {
return new AutocompleteItem(
keyword,
keyword, // searchName same as display name
keyword,
Kind.KEYWORD,
"keyword",
null, // TypeInfo for keyword
null,
null,
null,
false,
false,
null,
-1 // Keywords don't use inheritance depth sorting
);
}
/**
* Create from a synthetic method (e.g., Nashorn built-in like Java.type).
*/
public static AutocompleteItem fromSyntheticMethod(
SyntheticMethod method,
TypeInfo containingType) {
return fromSyntheticMethod(method, containingType, false);
}
/**
* Create from a synthetic method (e.g., Nashorn built-in like Java.type).
* @param method The synthetic method
* @param containingType The containing type info
* @param forMethodReference If true, insert text will NOT include parentheses
*/
public static AutocompleteItem fromSyntheticMethod(
SyntheticMethod method,
TypeInfo containingType,
boolean forMethodReference) {
String name = method.name;
StringBuilder insertText = new StringBuilder(name);
// For method references, don't add () since it's ::methodName not ::methodName()
if (!forMethodReference) {
insertText.append("(");
insertText.append(")");
}
// Build display name with parameters - use simple names for types
StringBuilder displayName = new StringBuilder(name);
displayName.append("(");
for (int i = 0; i < method.parameters.size(); i++) {
if (i > 0) displayName.append(", ");
SyntheticParameter param =
method.parameters.get(i);
// Use simple name for display (e.g., "Class" instead of "java.lang.Class")
String simpleTypeName = getSimpleTypeName(param.typeName);
displayName.append(simpleTypeName).append(" ").append(param.name);
}
displayName.append(")");
// Use simple name for return type display too
String simpleReturnType = getSimpleTypeName(method.returnType);
return new AutocompleteItem(
displayName.toString(),
name, // Search against just the method name
insertText.toString(),
Kind.METHOD,
simpleReturnType,
method.getReturnTypeInfo(), // TypeInfo for return type - would need to resolve from TypeResolver
method.getSignature(),
method.documentation,
method,
false,
false,
null,
0 // Synthetic types are at depth 0
);
}
/**
* Get simple name from a fully-qualified type name.
*/
private static String getSimpleTypeName(String typeName) {
if (typeName == null) return "void";
int lastDot = typeName.lastIndexOf('.');
return lastDot >= 0 ? typeName.substring(lastDot + 1) : typeName;
}
/**
* Create from a synthetic field (e.g., Nashorn built-in).
*/
public static AutocompleteItem fromSyntheticField(
SyntheticField field) {
return new AutocompleteItem(
field.name,
field.name, // searchName same as display name
field.name,
Kind.FIELD,
field.typeName,
field.getTypeInfo(), // TypeInfo for field type - would need to resolve from TypeResolver
field.typeName + " " + field.name,
field.documentation,
field,
false,
false,
null,
0 // Synthetic types are at depth 0
);
}
// ==================== HELPER METHODS ====================
private static String buildMethodSignature(MethodInfo method) {
StringBuilder sb = new StringBuilder();
String returnType = method.getReturnType() != null ?
getName(method.getReturnType()) : "void";
sb.append(returnType).append(" ").append(method.getName()).append("(");
for (int i = 0; i < method.getParameterCount(); i++) {
if (i > 0) sb.append(", ");
FieldInfo param = method.getParameters().get(i);
String paramType = param.getTypeInfo() != null ?
getName(param.getTypeInfo()) : "?";
sb.append(paramType);
if (param.isVarArg()) sb.append("...");
sb.append(" ").append(param.getName());
}
sb.append(")");
return sb.toString();
}
// ==================== MATCHING ====================
/**
* Calculate fuzzy match score against a query string.
* Returns -1 if no match, or a positive score (higher = better match).
*
* @param query The search query
* @param requirePrefix If true, only match items that start with the query (no fuzzy/contains matching)
*/
public int calculateMatchScore(String query, boolean requirePrefix) {
if (query == null || query.isEmpty()) {
matchScore = 100; // Everything matches empty query
matchIndices = new int[0];
return matchScore;
}
String lowerName = searchName.toLowerCase();
String lowerQuery = query.toLowerCase();
// Exact prefix match is best
if (lowerName.startsWith(lowerQuery)) {
matchScore = 1000 - query.length(); // Shorter prefixes score higher
matchIndices = new int[query.length()];
for (int i = 0; i < query.length(); i++) {
matchIndices[i] = i;
}
return matchScore;
}
// If requirePrefix is true, stop here - no fuzzy/substring matching
if (requirePrefix) {
matchScore = -1;
matchIndices = new int[0];
return matchScore;
}
// Exact substring match
int subIndex = lowerName.indexOf(lowerQuery);
if (subIndex >= 0) {
matchScore = 500 - subIndex; // Earlier substrings score higher
matchIndices = new int[query.length()];
for (int i = 0; i < query.length(); i++) {
matchIndices[i] = subIndex + i;
}
return matchScore;
}
// Fuzzy match - characters must appear in order
int[] indices = new int[query.length()];
int nameIdx = 0;
int queryIdx = 0;
int gaps = 0;
int consecutiveBonus = 0;
int lastMatchIdx = -2;
while (queryIdx < query.length() && nameIdx < searchName.length()) {
if (Character.toLowerCase(searchName.charAt(nameIdx)) == lowerQuery.charAt(queryIdx)) {
indices[queryIdx] = nameIdx;
// Bonus for consecutive matches
if (nameIdx == lastMatchIdx + 1) {
consecutiveBonus += 10;
}
// Bonus for matching at word boundaries (camelCase)
if (nameIdx == 0 || !Character.isLetterOrDigit(searchName.charAt(nameIdx - 1)) ||
(Character.isUpperCase(searchName.charAt(nameIdx)) && nameIdx > 0 &&
Character.isLowerCase(searchName.charAt(nameIdx - 1)))) {
consecutiveBonus += 20;
}
lastMatchIdx = nameIdx;
queryIdx++;
} else {
gaps++;
}
nameIdx++;
}
if (queryIdx < query.length()) {
// Didn't match all characters
matchScore = -1;
matchIndices = null;
return -1;
}
matchScore = 100 + consecutiveBonus - gaps;
matchIndices = indices;
return matchScore;
}
/**
* Backward compatibility overload - defaults to fuzzy matching (no prefix requirement).
*/
public int calculateMatchScore(String query) {
return calculateMatchScore(query, false);
}
/**
* Add a boost to the match score (e.g., from usage tracking).
*/
public void addScoreBoost(int boost) {
this.matchScore += boost;
}
// ==================== GETTERS ====================
public String getName() { return name; }
public String getSearchName() { return searchName; }
public String getInsertText() { return insertText; }
public Kind getKind() { return kind; }
public String getTypeLabel() { return typeLabel; }
public TypeInfo getTypeInfo() { return typeInfo; }
public String getSignature() { return signature; }
public String getDocumentation() { return documentation; }
public Object getSourceData() { return sourceData; }
public boolean isDeprecated() { return deprecated; }
public boolean requiresImport() { return requiresImport; }
public String getImportPath() { return importPath; }
public int getMatchScore() { return matchScore; }
public int[] getMatchIndices() { return matchIndices; }
/**
* Get the display name for a type, including generic arguments.
* Uses the unified getDisplayName() method which handles:
* - Simple types: "String"
* - Parameterized types: "List<String>", "Map<String, Integer>"
* - Nested generics: "List<Map<String, Integer>>"
* - JS types with namespace: "IPlayerEvent.InteractEvent"
*/
public static String getName(TypeInfo type) {
return type.getDisplayName();
}
public int getParameterCount() {
if (sourceData instanceof MethodInfo) {
return ((MethodInfo) sourceData).getParameterCount();
} else if (sourceData instanceof JSMethodInfo) {
return ((JSMethodInfo) sourceData).getParameterCount();
} else if (sourceData instanceof SyntheticMethod) {
return ((SyntheticMethod) sourceData).parameters.size();
}
return 0;
}
/**
* Check if this item represents a static member.
*/
public boolean isStatic() {
if (sourceData instanceof MethodInfo) {
return ((MethodInfo) sourceData).isStatic();
} else if (sourceData instanceof FieldInfo) {
return ((FieldInfo) sourceData).isStatic();
} else if (sourceData instanceof JSMethodInfo) {
return ((JSMethodInfo) sourceData).isStatic();
} else if (sourceData instanceof JSFieldInfo) {
return ((JSFieldInfo) sourceData).isStatic();
}
return false;
}
/**
* Check if this item represents a final member.
*/
public boolean isFinal() {
if (sourceData instanceof MethodInfo) {
return ((MethodInfo) sourceData).isFinal();
} else if (sourceData instanceof FieldInfo) {
return ((FieldInfo) sourceData).isFinal();
}else if (sourceData instanceof JSFieldInfo) {
return ((JSFieldInfo) sourceData).isReadonly();
}
return false;
}
/**
* Check if this method is inherited from Object class and NOT overridden.
* These methods should be deprioritized in autocomplete.
*/
public boolean isInheritedObjectMethod() {
if (kind != Kind.METHOD) return false;
if (sourceData instanceof MethodInfo) {
MethodInfo methodInfo = (MethodInfo) sourceData;
java.lang.reflect.Method javaMethod = methodInfo.getJavaMethod();
if (javaMethod != null) {
return Object.class.equals(javaMethod.getDeclaringClass());
}
TypeInfo containingType = methodInfo.getContainingType();
return containingType != null
&& containingType.getFullName().equals("java.lang.Object")
&& !methodInfo.isStatic()
&& !methodInfo.isOverride();
}
if (sourceData instanceof JSMethodInfo) {
JSMethodInfo jsMethod = (JSMethodInfo) sourceData;
JSTypeInfo definingType = jsMethod.getContainingType();
return definingType != null
&& "Object".equals(definingType.getSimpleName())
&& !jsMethod.isStatic()
&& inheritanceDepth > 0;
}
return false;
}
/**
* Get icon identifier based on kind.
*/
public String getIconId() {
switch (kind) {
case METHOD: return "m";
case FIELD: return "f";
case VARIABLE: return "v";
case CLASS: return "C";
case ENUM: return "E";
case ENUM_CONSTANT: return "e";
case KEYWORD: return "k";
case SNIPPET: return "s";
default: return "?";
}
}
/**
* Get color for the kind icon.
*/
public int getIconColor() {
switch (kind) {
case METHOD: return 0xFFB877DB; // Purple for methods
case FIELD: return 0xFF79C0FF; // Blue for fields
case VARIABLE: return 0xFF7EE787; // Green for variables
case CLASS: return 0xFFFFA657; // Orange for classes
case ENUM: return 0xFFFFD866; // Yellow for enums
case ENUM_CONSTANT: return 0xFFFFD866;
case KEYWORD: return 0xFFFF7B72; // Red for keywords
case SNIPPET: return 0xFFCCCCCC; // Gray for snippets
default: return 0xFFCCCCCC;
}
}
@Override
public int compareTo(AutocompleteItem other) {
// First by match score (descending) - this includes all penalties and boosts
if (this.matchScore != other.matchScore) {
return other.matchScore - this.matchScore;
}
// Then by kind priority
if (this.kind.getPriority() != other.kind.getPriority()) {
return this.kind.getPriority() - other.kind.getPriority();
}
// For JS types, prioritize by inheritance depth (child class members first)
// Only apply if both items have valid inheritance depth (>= 0)
if (this.inheritanceDepth >= 0 && other.inheritanceDepth >= 0) {
if (this.inheritanceDepth != other.inheritanceDepth) {
return this.inheritanceDepth - other.inheritanceDepth; // Lower depth = closer to child = higher priority
}
}
// Finally alphabetically
return this.name.compareToIgnoreCase(other.name);
}
@Override
public String toString() {
return name + " (" + kind + ")";
}
// ==================== BUILDER ====================
/**
* Builder for creating AutocompleteItem instances without source data.
*/
public static class Builder {
private String name;
private String searchName;
private String insertText;
private Kind kind = Kind.FIELD;
private String typeLabel = "";
private String signature;
private String documentation;
private Object sourceData;
private boolean deprecated = false;
private boolean requiresImport = false;
private String importPath = null;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder searchName(String searchName) {
this.searchName = searchName;
return this;
}
public Builder insertText(String insertText) {
this.insertText = insertText;
return this;
}
public Builder kind(Kind kind) {
this.kind = kind;
return this;
}
public Builder typeLabel(String typeLabel) {
this.typeLabel = typeLabel;
return this;
}
public Builder signature(String signature) {
this.signature = signature;
return this;
}
public Builder documentation(String documentation) {
this.documentation = documentation;
return this;
}
public Builder sourceData(Object sourceData) {
this.sourceData = sourceData;
return this;
}
public Builder deprecated(boolean deprecated) {
this.deprecated = deprecated;
return this;
}
public Builder requiresImport(boolean requiresImport) {
this.requiresImport = requiresImport;
return this;
}
public Builder importPath(String importPath) {
this.importPath = importPath;
return this;
}
public AutocompleteItem build() {
if (insertText == null) {
insertText = name;
}
return new AutocompleteItem(name, searchName, insertText, kind, typeLabel,
null, signature, documentation, sourceData, deprecated, requiresImport, importPath, -1);
}
}
}
@@ -0,0 +1,1000 @@
package noppes.npcs.client.gui.util.script.autocomplete;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.ScaledResolution;
import noppes.npcs.client.gui.util.GuiNPCInterface;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
import java.util.List;
/**
* UI component for displaying autocomplete suggestions.
* Styled similar to SearchReplaceBar with IntelliJ-like appearance.
*/
public class AutocompleteMenu extends Gui {
// ==================== CONSTANTS ====================
private static final int MAX_VISIBLE_ITEMS = 10;
private static final int ITEM_HEIGHT = 16;
private static final int PADDING = 4;
private static final int ICON_WIDTH = 16;
private static final int MIN_WIDTH = 200;
private static final int MAX_WIDTH = 400;
private static final int HINT_HEIGHT = 18;
// ==================== COLORS ====================
private static final int BG_COLOR = 0xFF2d2d30;
private static final int BORDER_COLOR = 0xFF3c3c3c;
private static final int SELECTED_BG = 0xFF094771;
private static final int HOVER_BG = 0xFF37373d;
private static final int TEXT_COLOR = 0xFFe0e0e0;
private static final int DIM_TEXT_COLOR = 0xFF808080;
private static final int HIGHLIGHT_COLOR = 0xFFFFD866;
private static final int HINT_BG_COLOR = 0xFF252526;
private static final int SCROLLBAR_BG = 0xFF3c3c3c;
private static final int SCROLLBAR_FG = 0xFF606060;
// ==================== STATE ====================
private boolean visible = false;
private List<AutocompleteItem> items = new ArrayList<>();
private int selectedIndex = 0;
private int scrollOffset = 0;
private int hoveredIndex = -1;
// Scrollbar drag state
private boolean isDraggingScrollbar = false;
private int dragStartY = 0;
private int dragStartScroll = 0;
// Panel pan/drag state
private boolean isDraggingPanel;
private int panelDragOffsetX, panelDragOffsetY;
private boolean panelPositionOverridden;
private int overriddenX, overriddenY;
private boolean hasPendingPanelDrag;
private int pendingDragStartX, pendingDragStartY;
private boolean pendingItemConfirm;
// Panel resize state
private boolean isResizingPanel;
private int resizeInitMouseX, resizeInitMouseY;
private int resizeInitW, resizeInitH;
private boolean panelSizeOverridden;
private int overriddenW, overriddenH;
private static final int MIN_RESIZE_W = 140;
private static final int MIN_RESIZE_H = 60;
private static final int DRAG_THRESHOLD = 4;
// ==================== POSITION ====================
private int x, y;
public GuiNPCInterface ownerGui;
private int width, height;
private int menuWidth;
private int visibleItemsCount; // Actual number of items that fit in the menu
// ==================== FONT ====================
private final FontRenderer font;
// ==================== CALLBACK ====================
private AutocompleteCallback callback;
/**
* Callback interface for autocomplete events.
*/
public interface AutocompleteCallback {
void onItemSelected(AutocompleteItem item);
void onDismiss();
}
public AutocompleteMenu() {
this.font = Minecraft.getMinecraft().fontRenderer;
this.visibleItemsCount = MAX_VISIBLE_ITEMS;
}
private int getVisibleItemCapacity() {
if (items == null || items.isEmpty())
return 0;
int cap = visibleItemsCount > 0 ? visibleItemsCount : MAX_VISIBLE_ITEMS;
if (!panelSizeOverridden) cap = Math.min(cap, MAX_VISIBLE_ITEMS);
return Math.max(1, Math.min(cap, items.size()));
}
private int getMaxScrollOffset() {
int cap = getVisibleItemCapacity();
if (cap <= 0)
return 0;
return Math.max(0, items.size() - cap);
}
public void setCallback(AutocompleteCallback callback) {
this.callback = callback;
}
// ==================== VISIBILITY ====================
/**
* Show the menu at the specified position with the given items.
*/
public void show(int x, int y, List<AutocompleteItem> items, int viewportWidth, int viewportHeight) {
this.items = items != null ? new ArrayList<>(items) : new ArrayList<>();
this.selectedIndex = 0;
this.scrollOffset = 0;
this.hoveredIndex = -1;
// Calculate menu dimensions
calculateDimensions(x, y, viewportWidth, viewportHeight);
this.visible = !this.items.isEmpty();
}
/**
* Update the items while keeping the menu open.
*/
public void updateItems(List<AutocompleteItem> newItems) {
this.items = newItems != null ? new ArrayList<>(newItems) : new ArrayList<>();
// Clamp selection
if (selectedIndex >= items.size()) {
selectedIndex = Math.max(0, items.size() - 1);
}
// Clamp scroll
int maxScroll = getMaxScrollOffset();
if (scrollOffset > maxScroll) {
scrollOffset = maxScroll;
}
visible = !items.isEmpty();
}
/**
* Hide the menu.
*/
public void hide() {
visible = false;
isDraggingPanel = false;
isResizingPanel = false;
panelPositionOverridden = false;
panelSizeOverridden = false;
hasPendingPanelDrag = false;
pendingItemConfirm = false;
if (callback != null) {
callback.onDismiss();
}
}
public boolean isVisible() {
return visible;
}
public boolean hasItems() {
return !items.isEmpty();
}
// ==================== DIMENSIONS ====================
private void calculateDimensions(int cursorX, int cursorY, int viewportWidth, int viewportHeight) {
// Calculate width based on item content
int maxItemWidth = MIN_WIDTH;
for (AutocompleteItem item : items) {
int itemWidth = ICON_WIDTH + PADDING + font.getStringWidth(item.getName()) + PADDING;
if (item.getTypeLabel() != null) {
itemWidth += font.getStringWidth(item.getTypeLabel()) + PADDING * 2;
}
maxItemWidth = Math.max(maxItemWidth, itemWidth);
}
menuWidth = Math.min(maxItemWidth + 20, MAX_WIDTH); // +20 for scrollbar
// Calculate initial height
int visibleItems = Math.min(items.size(), MAX_VISIBLE_ITEMS);
int menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2;
// Line height estimation (cursor position is typically at baseline)
int lineHeight = 20;
// Calculate available space
int spaceBelow = viewportHeight - cursorY - 10; // Space below cursor
int spaceAbove = cursorY - lineHeight - 10; // Space above current line
// Determine if we should use horizontal positioning
// Use horizontal if both vertical positions would be cramped or block the cursor line
boolean useHorizontalPosition = false;
if (spaceBelow < menuHeight && spaceAbove < menuHeight) {
// Check if horizontal positioning would work better
int spaceRight = viewportWidth - (cursorX + 50) - 10; // 50 = offset from cursor
if (spaceRight >= menuWidth && viewportHeight > menuHeight + 20) {
useHorizontalPosition = true;
} else {
// Reduce height to fit in the best available vertical space
int availableVerticalSpace = Math.max(spaceBelow, spaceAbove);
if (availableVerticalSpace < menuHeight) {
// Calculate how many items we can show in available space
int maxItemsForSpace = (availableVerticalSpace - HINT_HEIGHT - PADDING * 2) / ITEM_HEIGHT;
maxItemsForSpace = Math.max(3, Math.min(maxItemsForSpace, items.size())); // At least 3 items
visibleItems = maxItemsForSpace;
menuHeight = visibleItems * ITEM_HEIGHT + HINT_HEIGHT + PADDING * 2;
}
}
}
// Store the calculated visible items count
this.visibleItemsCount = visibleItems;
// Position the menu
if (useHorizontalPosition) {
// Position to the right of the cursor
this.x = cursorX + 50;
this.y = cursorY - lineHeight; // Align with current line
// Ensure doesn't go off bottom
if (y + menuHeight > viewportHeight - 10) {
y = viewportHeight - menuHeight - 10;
}
} else {
// Standard vertical positioning
this.x = cursorX;
// Try below first
if (spaceBelow >= menuHeight) {
this.y = cursorY ;
} else if (spaceAbove >= menuHeight) {
// Show above cursor line
this.y = cursorY - menuHeight - lineHeight;
} else {
// Use the larger space
if (spaceBelow >= spaceAbove) {
this.y = cursorY;
} else {
this.y = cursorY - menuHeight - lineHeight;
}
}
// Check if menu would go off-screen to the right
if (x + menuWidth > viewportWidth - 10) {
x = viewportWidth - menuWidth - 10;
}
}
// Ensure not off-screen to the left or top
x = Math.max(5, x);
y = Math.max(5, y);
this.width = menuWidth;
this.height = menuHeight;
// Apply position override if user has dragged the panel
if (panelPositionOverridden) { this.x = overriddenX; this.y = overriddenY; }
// Apply size override if user has resized the panel
if (panelSizeOverridden) {
this.menuWidth = overriddenW;
this.width = overriddenW;
this.height = overriddenH;
this.visibleItemsCount = Math.max(1, (overriddenH - HINT_HEIGHT - PADDING * 2) / ITEM_HEIGHT);
}
}
// ==================== SELECTION ====================
/**
* Move selection up.
*/
public void selectPrevious() {
if (items.isEmpty()) return;
selectedIndex--;
if (selectedIndex < 0) {
selectedIndex = items.size() - 1;
scrollOffset = getMaxScrollOffset();
} else if (selectedIndex < scrollOffset) {
scrollOffset = selectedIndex;
}
}
/**
* Move selection down.
*/
public void selectNext() {
if (items.isEmpty()) return;
selectedIndex++;
if (selectedIndex >= items.size()) {
selectedIndex = 0;
scrollOffset = 0;
} else {
int cap = getVisibleItemCapacity();
if (cap > 0 && selectedIndex >= scrollOffset + cap) {
scrollOffset = selectedIndex - cap + 1;
}
}
}
/**
* Confirm the current selection.
*/
public void confirmSelection() {
if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) {
hide();
return;
}
AutocompleteItem selected = items.get(selectedIndex);
if (callback != null) {
callback.onItemSelected(selected);
}
hide();
}
/**
* Get the currently selected item.
*/
public AutocompleteItem getSelectedItem() {
if (items.isEmpty() || selectedIndex < 0 || selectedIndex >= items.size()) {
return null;
}
return items.get(selectedIndex);
}
// ==================== DRAWING ====================
/**
* Draw the autocomplete menu.
*/
public void draw(int mouseX, int mouseY) {
if (!visible || items.isEmpty()) return;
// Update hover state
updateHoverState(mouseX, mouseY);
// Enable scissoring to clip content
GL11.glEnable(GL11.GL_SCISSOR_TEST);
setScissor(x - 2, y - 2, width + 4, height + 4);
// Draw background
drawRect(x, y, x + width, y + height, BG_COLOR);
// Draw border
drawRect(x, y, x + width, y + 1, BORDER_COLOR);
drawRect(x, y + height - 1, x + width, y + height, BORDER_COLOR);
drawRect(x, y, x + 1, y + height, BORDER_COLOR);
drawRect(x + width - 1, y, x + width, y + height, BORDER_COLOR);
// Draw items — tighter scissor so partial last item is clipped at the items-area boundary
int itemAreaH = height - HINT_HEIGHT - PADDING * 2;
setScissor(x + 1, y + PADDING , width - 2, itemAreaH);
int itemY = y + PADDING;
int visibleCount = getVisibleItemCapacity();
int renderCount = Math.min(visibleCount + 1, items.size() - scrollOffset);
for (int i = 0; i < renderCount; i++) {
int itemIndex = scrollOffset + i;
if (itemIndex >= items.size()) break;
AutocompleteItem item = items.get(itemIndex);
boolean isSelected = (itemIndex == selectedIndex);
boolean isHovered = (itemIndex == hoveredIndex);
drawItem(item, x + PADDING, itemY, width - PADDING * 2 - 8, isSelected, isHovered);
itemY += ITEM_HEIGHT;
}
// Restore full-panel scissor for scrollbar and hint bar
setScissor(x - 2, y - 2, width + 4, height + 4);
// Draw scrollbar if needed
if (items.size() > visibleCount) {
drawScrollbar(mouseX, mouseY);
}
// Draw hint bar at bottom
drawHintBar();
GL11.glDisable(GL11.GL_SCISSOR_TEST);
// Draw resize handle outside scissor
drawResizeHandle(mouseX, mouseY);
}
/**
* Draw a single autocomplete item.
*/
private void drawItem(AutocompleteItem item, int itemX, int itemY, int itemWidth,
boolean selected, boolean hovered) {
// Background
if (selected) {
drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, SELECTED_BG);
} else if (hovered) {
drawRect(itemX - 2, itemY, itemX + itemWidth + 2, itemY + ITEM_HEIGHT, HOVER_BG);
}
int textX = itemX;
int textY = itemY + (ITEM_HEIGHT - font.FONT_HEIGHT) / 2;
// Draw modifier text (static/final) if present
boolean isStatic = item.isStatic();
boolean isFinal = item.isFinal();
if (isStatic || isFinal) {
GL11.glPushMatrix();
float scale = 0.5f;
GL11.glScalef(scale, scale, scale);
int col = TokenType.MODIFIER.getHexColor();
if (isStatic)
font.drawString("s", (int) (textX / scale), (int) (textY / scale), col);
if (isFinal)
font.drawString("f", (int) (textX / scale), (int) (textY / scale) + 10, col);
GL11.glPopMatrix();
}
// Draw icon
String icon = item.getIconId();
int iconColor = item.getIconColor();
font.drawString(icon, textX + (ICON_WIDTH - font.getStringWidth(icon)) / 2, textY, iconColor);
textX += ICON_WIDTH;
// Calculate available width for text (leave space for type label)
int availableWidth = itemWidth - (textX - itemX);
if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) {
int typeLabelWidth = font.getStringWidth(item.getTypeLabel());
availableWidth -= typeLabelWidth + PADDING * 2;
}
// Determine text color - gray for inherited Object methods
int textColor = getColor(item);
if (item.getKind() == AutocompleteItem.Kind.METHOD) {
drawMethodNameTruncated(item.getName(), item.getMatchIndices(), textX, textY,
textColor, availableWidth);
} else {
drawHighlightedTextTruncated(item.getName(), item.getMatchIndices(), textX, textY,
textColor, availableWidth);
}
// Draw type label on the right
if (item.getTypeLabel() != null && !item.getTypeLabel().isEmpty()) {
String typeLabel = item.getTypeLabel();
int typeLabelWidth = font.getStringWidth(typeLabel);
int typeLabelX = itemX + itemWidth - typeLabelWidth - PADDING;
TypeInfo type = item.getTypeInfo();
int col = type != null ? type.getTokenType().getHexColor() : DIM_TEXT_COLOR;
drawTypeLabel(typeLabel, typeLabelX, textY, col, type);
}
}
public int getColor(AutocompleteItem item) {
if (item.isInheritedObjectMethod() || item.isDeprecated())
return DIM_TEXT_COLOR;
switch (item.getKind()) {
case METHOD:
return TokenType.METHOD_CALL.getHexColor();
case FIELD:
return TokenType.GLOBAL_FIELD.getHexColor();
case ENUM_CONSTANT:
return TokenType.ENUM_CONSTANT.getHexColor();
case CLASS:
return TokenType.getColor(item.getTypeInfo());
case VARIABLE:
return TokenType.LOCAL_FIELD.getHexColor();
case KEYWORD:
return TokenType.KEYWORD.getHexColor();
default:
return TEXT_COLOR;
}
}
/**
* Draw method name with parameters colored gray.
*/
private void drawMethodName(String text, int[] matchIndices, int x, int y, int baseColor) {
// Find the opening parenthesis
int parenIndex = text.indexOf('(');
if (parenIndex == -1) {
// No parameters, just draw normally
drawHighlightedText(text, matchIndices, x, y, baseColor);
return;
}
// Draw method name part with highlighting
String methodName = text.substring(0, parenIndex);
drawHighlightedText(methodName, matchIndices, x, y, baseColor);
// Draw parameters part in gray (no highlighting)
String params = text.substring(parenIndex);
int paramX = x + font.getStringWidth(methodName);
font.drawString(params, paramX, y, DIM_TEXT_COLOR);
}
/**
* Draw method name with parameters colored gray, truncated if too long.
*/
private void drawMethodNameTruncated(String text, int[] matchIndices, int x, int y, int baseColor, int maxWidth) {
// Find the opening parenthesis
int parenIndex = text.indexOf('(');
if (parenIndex == -1) {
// No parameters, just draw normally
drawHighlightedTextTruncated(text, matchIndices, x, y, baseColor, maxWidth);
return;
}
String methodName = text.substring(0, parenIndex);
String params = text.substring(parenIndex);
int methodNameWidth = font.getStringWidth(methodName);
int paramsWidth = font.getStringWidth(params);
int totalWidth = methodNameWidth + paramsWidth;
if (totalWidth <= maxWidth) {
// Fits, draw normally
drawHighlightedText(methodName, matchIndices, x, y, baseColor);
int paramX = x + methodNameWidth;
font.drawString(params, paramX, y, DIM_TEXT_COLOR);
} else {
// Need to truncate
String ellipsis = "...";
int ellipsisWidth = font.getStringWidth(ellipsis);
// Always show method name, truncate params if needed
if (methodNameWidth + ellipsisWidth < maxWidth) {
drawHighlightedText(methodName, matchIndices, x, y, baseColor);
int paramX = x + methodNameWidth;
// Truncate parameters
int availableForParams = maxWidth - methodNameWidth - ellipsisWidth;
String truncatedParams = truncateString(params, availableForParams);
font.drawString(truncatedParams + ellipsis, paramX, y, DIM_TEXT_COLOR);
} else {
// Even method name doesn't fit, truncate it too
int availableForMethod = maxWidth - ellipsisWidth;
String truncatedMethod = truncateString(methodName, availableForMethod);
drawHighlightedText(truncatedMethod, matchIndices, x, y, baseColor);
font.drawString(ellipsis, x + font.getStringWidth(truncatedMethod), y, baseColor);
}
}
}
/**
* Draw text with specific characters highlighted.
*/
private void drawHighlightedText(String text, int[] matchIndices, int x, int y, int baseColor) {
if (matchIndices == null || matchIndices.length == 0) {
font.drawString(text, x, y, baseColor);
return;
}
// Create a set of highlighted indices for quick lookup
java.util.Set<Integer> highlightSet = new java.util.HashSet<>();
for (int idx : matchIndices) {
highlightSet.add(idx);
}
// Draw character by character
int currentX = x;
for (int i = 0; i < text.length(); i++) {
String ch = String.valueOf(text.charAt(i));
int color = highlightSet.contains(i) ? HIGHLIGHT_COLOR : baseColor;
font.drawString(ch, currentX, y, color);
currentX += font.getStringWidth(ch);
}
}
/**
* Draw text with highlighting, truncated if too long.
*/
private void drawHighlightedTextTruncated(String text, int[] matchIndices, int x, int y, int baseColor, int maxWidth) {
int textWidth = font.getStringWidth(text);
if (textWidth <= maxWidth) {
// Fits, draw normally
drawHighlightedText(text, matchIndices, x, y, baseColor);
} else {
// Truncate with ellipsis
String ellipsis = "...";
int ellipsisWidth = font.getStringWidth(ellipsis);
String truncated = truncateString(text, maxWidth - ellipsisWidth);
// Adjust match indices for truncated text
int[] adjustedIndices = null;
if (matchIndices != null) {
java.util.List<Integer> validIndices = new java.util.ArrayList<>();
for (int idx : matchIndices) {
if (idx < truncated.length()) {
validIndices.add(idx);
}
}
adjustedIndices = new int[validIndices.size()];
for (int i = 0; i < validIndices.size(); i++) {
adjustedIndices[i] = validIndices.get(i);
}
}
drawHighlightedText(truncated, adjustedIndices, x, y, baseColor);
font.drawString(ellipsis, x + font.getStringWidth(truncated), y, baseColor);
}
}
/**
* Truncate a string to fit within the given width.
*/
private String truncateString(String text, int maxWidth) {
if (text.isEmpty()) return text;
int width = 0;
for (int i = 0; i < text.length(); i++) {
width += font.getStringWidth(String.valueOf(text.charAt(i)));
if (width > maxWidth) {
return text.substring(0, Math.max(0, i));
}
}
return text;
}
private void drawTypeLabel(String typeLabel, int x, int y, int typeColor, TypeInfo typeInfo) {
if (typeInfo == null) {
drawSimpleTypeWithArraySuffix(typeLabel, x, y, typeColor);
return;
}
drawTypeLabel(x, y, typeInfo, 0);
}
private int drawTypeLabel(int x, int y, TypeInfo typeInfo, int depth) {
if (depth > 25) {
String name = typeInfo.getDisplayName();
font.drawString(name, x, y, TokenType.getColor(typeInfo));
return x + font.getStringWidth(name);
}
int defaultColor = TokenType.DEFAULT.getHexColor();
if (typeInfo.isParameterized()) {
TypeInfo raw = typeInfo.getRawType();
x = drawSimpleTypeWithArraySuffix(raw.getDisplayName(), x, y, TokenType.getColor(raw));
font.drawString("<", x, y, defaultColor);
x += font.getStringWidth("<");
java.util.List<TypeInfo> args = typeInfo.getAppliedTypeArgs();
for (int i = 0; i < args.size(); i++) {
if (i > 0) {
font.drawString(", ", x, y, defaultColor);
x += font.getStringWidth(", ");
}
x = drawTypeLabel(x, y, args.get(i), depth + 1);
}
font.drawString(">", x, y, defaultColor);
return x + font.getStringWidth(">");
}
return drawSimpleTypeWithArraySuffix(typeInfo.getDisplayName(), x, y, TokenType.getColor(typeInfo));
}
/** @return x position after last drawn character */
private int drawSimpleTypeWithArraySuffix(String typeLabel, int x, int y, int typeColor) {
if (!typeLabel.endsWith("[]")) {
font.drawString(typeLabel, x, y, typeColor);
return x + font.getStringWidth(typeLabel);
}
int suffixStart = typeLabel.length() - 2;
while (suffixStart >= 2 && typeLabel.charAt(suffixStart - 2) == '[' && typeLabel.charAt(suffixStart - 1) == ']') {
suffixStart -= 2;
}
String core = typeLabel.substring(0, suffixStart);
String suffix = typeLabel.substring(suffixStart);
font.drawString(core, x, y, typeColor);
int coreWidth = font.getStringWidth(core);
font.drawString(suffix, x + coreWidth, y, TokenType.DEFAULT.getHexColor());
return x + coreWidth + font.getStringWidth(suffix);
}
private void drawScrollbar(int mouseX, int mouseY) {
int scrollbarX = x + width - 8;
int scrollbarY = y + PADDING;
int visibleCount = getVisibleItemCapacity();
if (visibleCount <= 0)
return;
int scrollbarHeight = visibleCount * ITEM_HEIGHT - 8; // leave room for resize handle
// Background
drawRect(scrollbarX, scrollbarY, scrollbarX + 6, scrollbarY + scrollbarHeight, SCROLLBAR_BG);
// Thumb
float thumbRatio = (float) visibleCount / items.size();
int thumbHeight = Math.max(20, (int) (scrollbarHeight * thumbRatio));
float thumbPosRatio = Math.min(1f, (float) scrollOffset / Math.max(1, items.size() - visibleCount));
int thumbY = scrollbarY + (int) ((scrollbarHeight - thumbHeight) * thumbPosRatio);
thumbY = Math.max(scrollbarY, Math.min(scrollbarY + scrollbarHeight - thumbHeight, thumbY));
// Check if mouse is above the scrollbar thumb
boolean isAboveScrollbar = mouseX >= scrollbarX && mouseX <= scrollbarX + 6 &&
mouseY >= thumbY && mouseY < thumbY + thumbHeight;
int col = isDraggingScrollbar || isAboveScrollbar? 0xFF808080 : SCROLLBAR_FG;
drawRect(scrollbarX + 1, thumbY, scrollbarX + 5, thumbY + thumbHeight, col);
}
/**
* Draw the hint bar at the bottom (shows Tab/Enter to confirm).
*/
private void drawHintBar() {
int hintY = y + height - HINT_HEIGHT;
// Darker background for hint area
drawRect(x + 1, hintY, x + width - 1, y + height - 1, HINT_BG_COLOR);
// Draw hints
int hintTextY = hintY + (HINT_HEIGHT - font.FONT_HEIGHT) / 2;
int hintX = x + PADDING;
// Tab hint
drawKeyHint("Tab", hintX, hintTextY);
hintX += font.getStringWidth("Tab") + 8;
font.drawString("or", hintX, hintTextY, DIM_TEXT_COLOR);
hintX += font.getStringWidth("or") + 4;
// Enter hint
drawKeyHint("Enter", hintX, hintTextY);
hintX += font.getStringWidth("Enter") + 8;
font.drawString("to insert", hintX, hintTextY, DIM_TEXT_COLOR);
}
/**
* Draw a key hint with a darker background box.
*/
private void drawKeyHint(String key, int x, int y) {
int keyWidth = font.getStringWidth(key);
int boxPadding = 2;
// Draw box
drawRect(x - boxPadding, y - boxPadding,
x + keyWidth + boxPadding, y + font.FONT_HEIGHT + boxPadding,
0xFF404040);
// Draw key text
font.drawString(key, x, y, TEXT_COLOR);
}
// ==================== MOUSE HANDLING ====================
/**
* Update hover state based on mouse position.
*/
private void updateHoverState(int mouseX, int mouseY) {
hoveredIndex = -1;
if (!isMouseInBounds(mouseX, mouseY)) return;
int itemY = y + PADDING;
int visibleItems = getVisibleItemCapacity();
for (int i = 0; i <= visibleItems; i++) {
int idx = scrollOffset + i;
if (idx >= items.size()) break;
if (mouseY >= itemY && mouseY < itemY + ITEM_HEIGHT) {
hoveredIndex = idx;
break;
}
itemY += ITEM_HEIGHT;
}
}
/**
* Handle mouse click.
* @return true if click was consumed
*/
public boolean mouseClicked(int mouseX, int mouseY, int button) {
if (!visible) return false;
// Check if click is outside menu
if (!isMouseInBounds(mouseX, mouseY)) {
hide();
return false;
}
// Resize handle has top priority
if (button == 0 && isMouseOverResizeHandle(mouseX, mouseY)) {
isResizingPanel = true;
resizeInitMouseX = mouseX; resizeInitMouseY = mouseY;
resizeInitW = width; resizeInitH = height;
return true;
}
// Scrollbar
if (button == 0 && items.size() > getVisibleItemCapacity()) {
int scrollbarX = x + width - 8;
if (mouseX >= scrollbarX && mouseX <= scrollbarX + 6) {
isDraggingScrollbar = true;
dragStartY = mouseY;
dragStartScroll = scrollOffset;
return true;
}
}
// Anywhere inside panel body — start pending drag, defer item confirm to release
if (button == 0) {
hasPendingPanelDrag = true;
pendingDragStartX = mouseX;
pendingDragStartY = mouseY;
panelDragOffsetX = mouseX - x;
panelDragOffsetY = mouseY - y;
if (hoveredIndex >= 0 && hoveredIndex < items.size()) {
selectedIndex = hoveredIndex;
pendingItemConfirm = true;
}
return true;
}
return true; // Consume click if inside menu
}
/**
* Handle mouse release.
* @return true if release was consumed
*/
public boolean mouseReleased(int mouseX, int mouseY, int button) {
if (button == 0) {
if (isDraggingScrollbar) { isDraggingScrollbar = false; return true; }
if (isDraggingPanel) { isDraggingPanel = false; return true; }
if (isResizingPanel) { isResizingPanel = false; return true; }
hasPendingPanelDrag = false;
if (pendingItemConfirm) {
pendingItemConfirm = false;
confirmSelection();
return true;
}
}
return false;
}
/**
* Handle mouse drag.
* @return true if drag was consumed
*/
public boolean mouseDragged(int mouseX, int mouseY) {
if (!visible) return false;
if (isDraggingPanel) { updatePanelDrag(mouseX, mouseY); return true; }
if (isResizingPanel) { updatePanelResize(mouseX, mouseY); return true; }
// Activate panel drag once movement exceeds threshold
if (hasPendingPanelDrag) {
int dx = Math.abs(mouseX - pendingDragStartX);
int dy = Math.abs(mouseY - pendingDragStartY);
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
isDraggingPanel = true;
hasPendingPanelDrag = false;
pendingItemConfirm = false; // cancel any pending selection
updatePanelDrag(mouseX, mouseY);
return true;
}
return true;
}
if (!isDraggingScrollbar) return false;
// Scrollbar drag (kept for backward compat — also handled via draw loop)
int visibleCount = getVisibleItemCapacity();
if (visibleCount <= 0) return false;
int scrollbarHeight = visibleCount * ITEM_HEIGHT - 8;
int thumbHeight = Math.max(20, (int) (scrollbarHeight * (float) visibleCount / Math.max(1, items.size())));
int scrollTrackHeight = Math.max(1, scrollbarHeight - thumbHeight);
int maxScroll = items.size() - visibleCount;
int deltaY = mouseY - dragStartY;
int scrollDelta = (deltaY * maxScroll) / scrollTrackHeight;
scrollOffset = Math.max(0, Math.min(maxScroll, dragStartScroll + scrollDelta));
return true;
}
/**
* Handle mouse scroll.
* @return true if scroll was consumed
*/
public boolean mouseScrolled(int mouseX, int mouseY, int delta) {
if (!visible || !isMouseInBounds(mouseX, mouseY)) return false;
int visibleCount = getVisibleItemCapacity();
if (items.size() > visibleCount) {
if (delta > 0) {
scrollOffset = Math.max(0, scrollOffset - 1);
} else {
scrollOffset = Math.min(items.size() - visibleCount, scrollOffset + 1);
}
return true;
}
return false;
}
private boolean isMouseInBounds(int mouseX, int mouseY) {
return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height;
}
// ==================== UTILITY ====================
private void setScissor(int x, int y, int width, int height) {
Minecraft mc = Minecraft.getMinecraft();
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int scaleFactor = sr.getScaleFactor();
if (ownerGui != null) {
x -= (int) ownerGui.getPanX();
y -= (int) ownerGui.getPanY();
}
int scissorX = x * scaleFactor;
int scissorY = mc.displayHeight - (y + height) * scaleFactor;
int scissorW = width * scaleFactor;
int scissorH = height * scaleFactor;
GL11.glScissor(scissorX, scissorY, scissorW, scissorH);
}
// ==================== GETTERS ====================
public int getX() { return x; }
public int getY() { return y; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public List<AutocompleteItem> getItems() { return items; }
// ==================== PANEL DRAG / RESIZE ====================
public boolean isMouseOverResizeHandle(int mx, int my) {
if (!visible) return false;
return mx >= x + width - 8 && mx <= x + width && my >= y + height - 8 && my <= y + height;
}
public void updatePanelDrag(int mouseX, int mouseY) {
if (!isDraggingPanel) return;
x = mouseX - panelDragOffsetX;
y = mouseY - panelDragOffsetY;
overriddenX = x;
overriddenY = y;
panelPositionOverridden = true;
}
public void releasePanelDrag() { isDraggingPanel = false; hasPendingPanelDrag = false; }
public void updatePanelResize(int mouseX, int mouseY) {
if (!isResizingPanel) return;
overriddenW = Math.max(MIN_RESIZE_W, resizeInitW + (mouseX - resizeInitMouseX));
overriddenH = Math.max(MIN_RESIZE_H, resizeInitH + (mouseY - resizeInitMouseY));
panelSizeOverridden = true;
menuWidth = overriddenW;
width = overriddenW;
height = overriddenH;
visibleItemsCount = Math.max(1, (overriddenH - HINT_HEIGHT - PADDING * 2) / ITEM_HEIGHT);
}
public void releasePanelResize() { isResizingPanel = false; }
/** Used by draw-loop scrollbar update in GuiScriptTextArea */
public void updateScrollbarDragDraw(int mouseY) {
if (!isDraggingScrollbar || items.isEmpty()) return;
int visibleCount = getVisibleItemCapacity();
if (visibleCount <= 0) return;
int scrollbarHeight = visibleCount * ITEM_HEIGHT - 8;
int thumbHeight = Math.max(20, (int) (scrollbarHeight * (float) visibleCount / items.size()));
int scrollTrackHeight = Math.max(1, scrollbarHeight - thumbHeight);
int maxScroll = items.size() - visibleCount;
int deltaY = mouseY - dragStartY;
int scrollDelta = (deltaY * maxScroll) / scrollTrackHeight;
scrollOffset = Math.max(0, Math.min(maxScroll, dragStartScroll + scrollDelta));
}
public void releaseScrollbarDrag() { isDraggingScrollbar = false; }
public boolean isDraggingPanel() { return isDraggingPanel; }
public boolean isDraggingScrollbarMenu() { return isDraggingScrollbar; }
public boolean isResizingPanel() { return isResizingPanel; }
private void drawResizeHandle(int mouseX, int mouseY) {
boolean resizeActive = isResizingPanel || isMouseOverResizeHandle(mouseX, mouseY);
int dotColor = resizeActive ? 0xFFCCCCCC : 0x80888888;
int rhX = x + width - 2;
int rhY = y + height - 2;
// Three-dot diagonal pattern (◢)
drawRect(rhX - 1, rhY - 1, rhX, rhY, dotColor);
drawRect(rhX - 3, rhY - 1, rhX - 2, rhY, dotColor);
drawRect(rhX - 1, rhY - 3, rhX, rhY - 2, dotColor);
drawRect(rhX - 5, rhY - 1, rhX - 4, rhY, dotColor);
drawRect(rhX - 3, rhY - 3, rhX - 2, rhY - 2, dotColor);
drawRect(rhX - 1, rhY - 5, rhX, rhY - 4, dotColor);
}
}
@@ -0,0 +1,74 @@
package noppes.npcs.client.gui.util.script.autocomplete;
import java.util.List;
/**
* Abstract provider for autocomplete suggestions.
* Implementations handle different contexts (after dot, in identifier, etc.)
*/
public interface AutocompleteProvider {
/**
* Context information for autocomplete requests.
*/
class Context {
/** Full text of the document */
public final String text;
/** Current cursor position in the document */
public final int cursorPosition;
/** The current line number (0-indexed) */
public final int lineNumber;
/** Position of cursor within the current line */
public final int columnPosition;
/** The current line text */
public final String currentLine;
/** The word being typed (prefix before cursor, after last separator) */
public final String prefix;
/** Start position of the prefix in the document */
public final int prefixStart;
/** Whether this is after a dot (member access) */
public final boolean isMemberAccess;
/** Expression before the dot (for member access) */
public final String receiverExpression;
/** Whether autocomplete was explicitly triggered (CTRL+Space) */
public final boolean explicitTrigger;
/** Whether to show only methods (for method reference context ::) */
public final boolean methodsOnly;
public Context(String text, int cursorPosition, int lineNumber, int columnPosition,
String currentLine, String prefix, int prefixStart,
boolean isMemberAccess, String receiverExpression, boolean explicitTrigger) {
this(text, cursorPosition, lineNumber, columnPosition, currentLine, prefix, prefixStart,
isMemberAccess, receiverExpression, explicitTrigger, false);
}
public Context(String text, int cursorPosition, int lineNumber, int columnPosition,
String currentLine, String prefix, int prefixStart,
boolean isMemberAccess, String receiverExpression, boolean explicitTrigger,
boolean methodsOnly) {
this.text = text;
this.cursorPosition = cursorPosition;
this.lineNumber = lineNumber;
this.columnPosition = columnPosition;
this.currentLine = currentLine;
this.prefix = prefix;
this.prefixStart = prefixStart;
this.isMemberAccess = isMemberAccess;
this.receiverExpression = receiverExpression;
this.explicitTrigger = explicitTrigger;
this.methodsOnly = methodsOnly;
}
}
/**
* Get autocomplete suggestions for the given context.
* @param context The context containing cursor position, text, etc.
* @return List of autocomplete items, sorted by relevance
*/
List<AutocompleteItem> getSuggestions(Context context);
/**
* Check if this provider can handle the given context.
*/
boolean canProvide(Context context);
}
@@ -0,0 +1,354 @@
package noppes.npcs.client.gui.util.script.autocomplete;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.*;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.*;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.config.ConfigScript;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Autocomplete provider for JavaScript/ECMAScript scripts.
* Uses ScriptDocument's unified TypeInfo/TypeResolver system for type resolution.
*/
public class JSAutocompleteProvider extends JavaAutocompleteProvider {
private final JSTypeRegistry registry;
public JSAutocompleteProvider() {
this.registry = JSTypeRegistry.getInstance();
if (!registry.isInitialized()) {
registry.initializeFromResources();
}
}
@Override
public boolean canProvide(Context context) {
return document != null && document.isJavaScript();
}
/**
* Add suggestions for member access (after dot).
* Uses ScriptDocument's unified type resolution system.
*/
protected void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
String receiverExpr = context.receiverExpression;
if (receiverExpr == null || receiverExpr.isEmpty()) {
return;
}
// Use ScriptDocument's resolveExpressionType - handles both Java and JS
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context));
if (receiverType == null || !receiverType.isResolved()) {
return;
}
// Check for synthetic types first (e.g., Nashorn Java object, custom types)
SyntheticType syntheticType =
document.getTypeResolver().getSyntheticType(receiverType.getSimpleName());
if (syntheticType != null) {
addSyntheticTypeSuggestions(syntheticType, items, context.methodsOnly);
return;
}
boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context));
Class<?> javaClass = receiverType.getJavaClass();
if (javaClass != null) {
Set<String> addedMethods = new HashSet<>();
for (Method method : javaClass.getMethods()) {
if (!Modifier.isPublic(method.getModifiers())) continue;
if (isStaticContext && !Modifier.isStatic(method.getModifiers())) continue;
String sig = method.getName() + "(" + method.getParameterCount() + ")";
if (addedMethods.add(sig)) {
items.add(AutocompleteItem.fromMethod(MethodInfo.fromReflection(method, receiverType), context.methodsOnly));
}
}
List<MethodInfo> effectiveSyntheticMethods = receiverType.getSyntheticMethods();
if (!effectiveSyntheticMethods.isEmpty()) {
for (MethodInfo method : effectiveSyntheticMethods) {
if (isStaticContext && !method.isStatic()) continue;
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
} else {
JSTypeInfo jsType = getMergeRegistryType(javaClass);
if (jsType != null) {
addMethodsFromType(jsType, receiverType, items, new HashSet<>(), context.methodsOnly, isStaticContext);
}
}
if (!context.methodsOnly) {
for (Field field : javaClass.getFields()) {
if (!Modifier.isPublic(field.getModifiers())) continue;
if (isStaticContext && !Modifier.isStatic(field.getModifiers())) continue;
items.add(AutocompleteItem.fromField(FieldInfo.fromReflection(field, receiverType)));
}
if (!receiverType.getSyntheticFields().isEmpty()) {
for (FieldInfo field : receiverType.getSyntheticFields()) {
if (isStaticContext && !field.isStatic()) continue;
items.add(AutocompleteItem.fromField(field));
}
} else {
JSTypeInfo jsType = getMergeRegistryType(javaClass);
if (jsType != null) {
addFieldsFromType(jsType, receiverType, items, new HashSet<>(), isStaticContext);
}
}
}
if (!isStaticContext && javaClass != Object.class) {
JSTypeInfo objJSType = registry.getType("Object");
if (objJSType != null) {
for (JSMethodInfo m : objJSType.getMethods().values()) {
if (!m.isStatic()) {
items.add(AutocompleteItem.fromJSMethod(m, receiverType, 1, context.methodsOnly));
}
}
}
}
return;
}
Set<String> syntheticMethodNames = new HashSet<>();
for (MethodInfo method : receiverType.getSyntheticMethods()) {
syntheticMethodNames.add(method.getName());
if (isStaticContext && !method.isStatic()) continue;
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
for (FieldInfo field : receiverType.getSyntheticFields()) {
if (!context.methodsOnly) {
if (isStaticContext && !field.isStatic()) continue;
if (syntheticMethodNames.contains(field.getName())) continue;
items.add(AutocompleteItem.fromField(field));
}
}
JSTypeInfo jsTypeInfo = receiverType.getJSTypeInfo();
if (jsTypeInfo != null) {
addMethodsFromType(jsTypeInfo, receiverType, items, new HashSet<>(), context.methodsOnly, isStaticContext);
if (!context.methodsOnly && ConfigScript.ShowImplementationFieldsInAutocomplete) {
addFieldsFromType(jsTypeInfo, receiverType, items, new HashSet<>(), isStaticContext);
}
}
if (!isStaticContext) {
Set<String> addedObjMethods = new HashSet<>();
for (Method m : Object.class.getMethods()) {
if (!Modifier.isPublic(m.getModifiers()) || Modifier.isStatic(m.getModifiers())) continue;
String sig = m.getName() + "(" + m.getParameterCount() + ")";
if (addedObjMethods.add(sig)) {
items.add(AutocompleteItem.fromMethod(MethodInfo.fromReflection(m, receiverType), context.methodsOnly));
}
}
JSTypeInfo objJSType = registry.getType("Object");
if (objJSType != null) {
for (JSMethodInfo m : objJSType.getMethods().values()) {
if (!m.isStatic()) {
items.add(AutocompleteItem.fromJSMethod(m, receiverType, 1, context.methodsOnly));
}
}
}
}
}
private JSTypeInfo getMergeRegistryType(Class<?> javaClass) {
return document.getTypeResolver().findJSTypeForJavaClass(javaClass);
}
/**
* Add autocomplete suggestions for a synthetic type's methods and fields.
* @param syntheticType The synthetic type
* @param items The list to add items to
* @param forMethodReference If true, only add methods with no parentheses in insert text
*/
private void addSyntheticTypeSuggestions(SyntheticType syntheticType, List<AutocompleteItem> items, boolean forMethodReference) {
// Add methods
for (SyntheticMethod method : syntheticType.getMethods()) {
AutocompleteItem item = AutocompleteItem.fromSyntheticMethod(method, syntheticType.getTypeInfo(), forMethodReference);
items.add(item);
}
// Add fields (skip if forMethodReference is true)
if (!forMethodReference) {
for (SyntheticField field : syntheticType.getFields()) {
AutocompleteItem item = AutocompleteItem.fromSyntheticField(field);
items.add(item);
}
}
}
/**
* Recursively add methods from a type and its parents.
* @param type The current JS type to get methods from (changes as we walk up inheritance)
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
*/
protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added) {
addMethodsFromType(type, contextType, items, added, 0, false, false);
}
/**
* Recursively add methods from a type and its parents.
* @param type The current JS type to get methods from (changes as we walk up inheritance)
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
* @param forMethodReference If true, insert text will NOT include parentheses
*/
protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, boolean forMethodReference) {
addMethodsFromType(type, contextType, items, added, 0, forMethodReference, false);
}
/**
* Recursively add methods from a type and its parents.
* @param type The current JS type to get methods from (changes as we walk up inheritance)
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
* @param forMethodReference If true, insert text will NOT include parentheses
* @param isStaticContext If true, only add static methods
*/
protected void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, boolean forMethodReference, boolean isStaticContext) {
addMethodsFromType(type, contextType, items, added, 0, forMethodReference, isStaticContext);
}
/**
* Recursively add methods from a type and its parents with inheritance depth tracking.
* Shows all overloads - one autocomplete item per overload.
* @param type The current JS type in inheritance chain
* @param contextType The original TypeInfo context for resolving type parameters (e.g., IPlayer with T → EntityPlayerMP)
*/
private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth) {
addMethodsFromType(type, contextType, items, added, depth, false, false);
}
/**
* Recursively add methods from a type and its parents with inheritance depth tracking.
* Shows all overloads - one autocomplete item per overload.
* @param type The current JS type in inheritance chain
* @param contextType The original TypeInfo context for resolving type parameters (e.g., IPlayer with T → EntityPlayerMP)
* @param forMethodReference If true, insert text will NOT include parentheses
*/
private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth, boolean forMethodReference) {
addMethodsFromType(type, contextType, items, added, depth, forMethodReference, false);
}
private void addMethodsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth, boolean forMethodReference, boolean isStaticContext) {
// Collect base method names (without $N suffix) that we haven't processed yet
Set<String> baseNames = new HashSet<>();
for (String key : type.getMethods().keySet()) {
String baseName = key.contains("$") ? key.substring(0, key.indexOf('$')) : key;
if (!added.contains(baseName)) {
baseNames.add(baseName);
}
}
// For each base method name, add all overloads
for (String baseName : baseNames) {
added.add(baseName);
// Use getMethodOverloads to get all overloads for this method (excluding inherited)
List<JSMethodInfo> overloads = new ArrayList<>();
if (type.getMethods().containsKey(baseName)) {
overloads.add(type.getMethods().get(baseName));
}
int index = 1;
while (type.getMethods().containsKey(baseName + "$" + index)) {
overloads.add(type.getMethods().get(baseName + "$" + index));
index++;
}
// Add one autocomplete item per overload
for (JSMethodInfo method : overloads) {
if (isStaticContext && !method.isStatic()) continue;
items.add(AutocompleteItem.fromJSMethod(method, contextType, depth, forMethodReference));
}
}
// Add from parent type with incremented depth - keep same contextType
if (type.getResolvedParent() != null) {
addMethodsFromType(type.getResolvedParent(), contextType, items, added, depth + 1, forMethodReference, isStaticContext);
}
}
/**
* Recursively add fields from a type and its parents.
* @param type The current JS type to get fields from (changes as we walk up inheritance)
* @param contextType The original TypeInfo context for resolving type parameters (stays constant)
*/
protected void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added) {
addFieldsFromType(type, contextType, items, added, 0, false);
}
protected void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, boolean isStaticContext) {
addFieldsFromType(type, contextType, items, added, 0, isStaticContext);
}
/**
* Recursively add fields from a type and its parents with inheritance depth tracking.
* @param type The current JS type in inheritance chain
* @param contextType The original TypeInfo context for resolving type parameters
*/
private void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth) {
addFieldsFromType(type, contextType, items, added, depth, false);
}
private void addFieldsFromType(JSTypeInfo type, TypeInfo contextType, List<AutocompleteItem> items, Set<String> added, int depth, boolean isStaticContext) {
for (JSFieldInfo field : type.getFields().values()) {
if (!added.contains(field.getName())) {
if (isStaticContext && !field.isStatic()) continue;
added.add(field.getName());
// Pass contextType (original receiver) for type parameter resolution, not current type
items.add(AutocompleteItem.fromJSField(field, contextType, depth));
}
}
// Add from parent type with incremented depth - keep same contextType
if (type.getResolvedParent() != null) {
addFieldsFromType(type.getResolvedParent(), contextType, items, added, depth + 1, isStaticContext);
}
}
protected void addUnimportedClassSuggestions(String prefix, List<AutocompleteItem> items) {
// For JavaScript, we typically don't suggest unimported classes
// as imports are handled differently. This can be customized
// if needed to suggest global JS types.
}
@Override
protected void addLanguageUniqueSuggestions(Context context, List<AutocompleteItem> items) {
super.addLanguageUniqueSuggestions(context, items);
// Add global variables from both global engine objects and document editor/DataScript globals
List<String> globalNames = new ArrayList<>();
globalNames.addAll(registry.getGlobalEngineObjects().keySet());
globalNames.addAll(document.getEditorGlobals().keySet());
for (String name : globalNames) {
FieldInfo fieldInfo = document.resolveVariable(name, context.cursorPosition);
if (fieldInfo == null || !fieldInfo.isResolved())
continue;
items.add(AutocompleteItem.fromField(fieldInfo));
}
// Add global imports in JSTypeRegistry.globalEngineImports
for (String importName : registry.getGlobalEngineImports().keySet()) {
JSTypeInfo jsTypeInfo = registry.getGlobalImportType(importName);
if (jsTypeInfo != null) {
items.add(AutocompleteItem.fromType(TypeInfo.fromJSTypeInfo(jsTypeInfo)));
}
}
// Add global engine functions (parseInt, parseFloat, isNaN, isFinite, etc.)
for (JSMethodInfo fn : registry.getGlobalEngineFunctions().values()) {
items.add(AutocompleteItem.fromJSMethod(fn, 0));
}
}
protected UsageTracker getUsageTracker() {
return UsageTracker.getJSInstance();
}
public String[] getKeywords() {
return TypeChecker.getJavaScriptKeywords();
}
}
@@ -0,0 +1,605 @@
package noppes.npcs.client.gui.util.script.autocomplete;
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Autocomplete provider for Java/Groovy scripts.
* Uses ScriptDocument for type resolution and scope analysis.
*/
public class JavaAutocompleteProvider implements AutocompleteProvider {
protected ScriptDocument document;
public void setDocument(ScriptDocument document) {
this.document = document;
}
@Override
public boolean canProvide(Context context) {
return document != null && !document.isJavaScript();
}
@Override
public List<AutocompleteItem> getSuggestions(Context context) {
List<AutocompleteItem> items = new ArrayList<>();
if (document == null) {
return items;
}
// Resolve owner type for usage tracking and static context detection
String ownerFullName = null;
boolean isStaticContext = false;
if (context.isMemberAccess && context.receiverExpression != null) {
int resolvePos = getMemberAccessResolvePosition(context);
TypeInfo receiverType = document.resolveExpressionType(
context.receiverExpression, resolvePos);
if (receiverType != null && receiverType.isResolved()) {
ownerFullName = receiverType.getFullName();
}
// Check if accessing through a class (static context) vs instance
isStaticContext = isStaticAccess(context.receiverExpression, resolvePos);
}
if (context.isMemberAccess) {
// Member access: resolve type of receiver and get its members
addMemberSuggestions(context, items);
} else {
// Identifier context: show variables, methods, types in scope
addScopeSuggestions(context, items);
}
// For method reference context, filter to show only methods (and constructor "new")
if (context.methodsOnly) {
items.removeIf(item -> item.getKind() != AutocompleteItem.Kind.METHOD);
// Add "new" constructor reference option if the receiver is a class
if (context.receiverExpression != null) {
int resolvePos = getMemberAccessResolvePosition(context);
TypeInfo receiverType = document.resolveExpressionType(
context.receiverExpression, resolvePos);
if (receiverType != null && receiverType.isResolved() && isStaticContext) {
items.add(new AutocompleteItem.Builder()
.name("new")
.insertText("new")
.kind(AutocompleteItem.Kind.KEYWORD)
.typeLabel("constructor")
.signature(receiverType.getSimpleName() + "::new")
.build());
}
}
}
// Filter and score by prefix, then apply usage boosts and static penalties
filterAndScore(items, context.prefix, context.isMemberAccess, isStaticContext, ownerFullName);
// Sort by score
Collections.sort(items);
return items;
}
/**
* Add suggestions for member access (after dot).
*/
protected void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
String receiverExpr = context.receiverExpression;
if (receiverExpr == null || receiverExpr.isEmpty()) {
return;
}
// Resolve the type of the receiver expression
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context));
if (receiverType == null || !receiverType.isResolved()) {
return;
}
// Determine if this is a static context (accessing a class type)
boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context));
Class<?> clazz = receiverType.getJavaClass();
if (clazz == null) {
// Try ScriptTypeInfo
if (receiverType instanceof ScriptTypeInfo) {
addScriptTypeMembers((ScriptTypeInfo) receiverType, items, isStaticContext, context.methodsOnly);
}
return;
}
// Add methods
Set<String> addedMethods = new HashSet<>();
for (Method method : clazz.getMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
// Filter by static context
if (isStaticContext && !Modifier.isStatic(method.getModifiers())) {
continue;
}
String sig = method.getName() + "(" + method.getParameterCount() + ")";
if (!addedMethods.contains(sig)) {
addedMethods.add(sig);
MethodInfo methodInfo = MethodInfo.fromReflection(method, receiverType);
items.add(AutocompleteItem.fromMethod(methodInfo, context.methodsOnly));
}
}
}
for (MethodInfo method : receiverType.getSyntheticMethods()) {
if (isStaticContext && !Modifier.isStatic(method.getModifiers()))
continue;
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
// Add fields (skip if methodsOnly is true - but the filtering will be done in getSuggestions)
if (!context.methodsOnly) {
for (Field field : clazz.getFields()) {
if (Modifier.isPublic(field.getModifiers())) {
// Filter by static context
if (isStaticContext && !Modifier.isStatic(field.getModifiers())) {
continue;
}
FieldInfo fieldInfo = FieldInfo.fromReflection(field, receiverType);
items.add(AutocompleteItem.fromField(fieldInfo));
}
}
for (FieldInfo field : receiverType.getSyntheticFields()) {
if (isStaticContext && !Modifier.isStatic(field.getModifiers()))
continue;
items.add(AutocompleteItem.fromField(field));
}
}
// Add nested types (inner classes/interfaces) in static context
if (isStaticContext && !context.methodsOnly) {
try {
for (Class<?> nested : clazz.getDeclaredClasses()) {
if (!Modifier.isPublic(nested.getModifiers())) {
continue;
}
// Convert $ notation to dot notation for resolution
String nestedFullName = nested.getName().replace('$', '.');
TypeInfo nestedType = TypeResolver.getInstance().resolveFullName(nestedFullName);
if (nestedType != null && nestedType.isResolved()) {
items.add(AutocompleteItem.fromType(nestedType));
}
}
} catch (SecurityException ignored) {
// Some runtimes may restrict getDeclaredClasses()
}
}
}
/**
* Pick a stable position for resolving member-access receiver types.
*
* Using `prefixStart` can land on end-exclusive scope boundaries (position == bodyEnd)
* depending on caller cursor semantics and surrounding syntax, causing scope lookups
* to fail. For member access we prefer the dot position (or nearest non-whitespace)
* immediately before prefixStart.
*/
protected int getMemberAccessResolvePosition(Context context) {
if (context == null || context.text == null || context.text.isEmpty()) {
return 0;
}
int pos = Math.max(0, Math.min(context.prefixStart, context.text.length()));
int i = Math.min(pos - 1, context.text.length() - 1);
// Skip whitespace backwards
while (i >= 0 && Character.isWhitespace(context.text.charAt(i))) {
i--;
}
// Prefer the dot index if present
if (i >= 0 && context.text.charAt(i) == '.') {
return i;
}
// Fallback: use the provided prefixStart (caret context)
return Math.max(0, Math.min(context.prefixStart, context.text.length()));
}
/**
* Add members from a script-defined type.
*/
protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List<AutocompleteItem> items, boolean isStaticContext) {
addScriptTypeMembers(scriptType, items, isStaticContext, false);
}
/**
* Add members from a script-defined type.
* @param scriptType The script type to get members from
* @param items The list to add items to
* @param isStaticContext Whether we're in a static context
* @param forMethodReference If true, only add methods with no parentheses in insert text
*/
protected void addScriptTypeMembers(ScriptTypeInfo scriptType, List<AutocompleteItem> items, boolean isStaticContext, boolean forMethodReference) {
// Add methods (getMethods returns Map<String, List<MethodInfo>>)
for (List<MethodInfo> overloads : scriptType.getMethods().values()) {
for (MethodInfo method : overloads) {
// Filter by static context
if (isStaticContext && !method.isStatic()) {
continue;
}
items.add(AutocompleteItem.fromMethod(method, forMethodReference));
}
}
// Add fields (skip if methodsOnly/forMethodReference is true)
if (!forMethodReference) {
// Add fields (getFields returns Map<String, FieldInfo>)
for (FieldInfo field : scriptType.getFields().values()) {
// Filter by static context
if (isStaticContext && !field.isStatic()) {
continue;
}
items.add(AutocompleteItem.fromField(field));
}
// Add enum constants (getEnumConstants returns Map<String, EnumConstantInfo>)
for (EnumConstantInfo enumConstant : scriptType.getEnumConstants().values()) {
items.add(AutocompleteItem.fromField(enumConstant.getFieldInfo()));
}
// Add nested types (inner classes/interfaces) in static context
if (isStaticContext) {
for (ScriptTypeInfo inner : scriptType.getInnerClasses()) {
items.add(AutocompleteItem.fromType(inner));
}
}
}
// Add parent class members
if (scriptType.hasSuperClass()) {
TypeInfo superType = scriptType.getSuperClass();
if (superType != null && superType.getJavaClass() != null) {
Class<?> superClazz = superType.getJavaClass();
Set<String> addedMethods = new HashSet<>();
for (Method method : superClazz.getMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
String sig = method.getName() + "(" + method.getParameterCount() + ")";
if (!addedMethods.contains(sig)) {
addedMethods.add(sig);
MethodInfo methodInfo = MethodInfo.fromReflection(method, superType);
items.add(AutocompleteItem.fromMethod(methodInfo, forMethodReference));
}
}
}
}
}
}
/**
* Add suggestions based on current scope (not after a dot).
*/
protected void addScopeSuggestions(Context context, List<AutocompleteItem> items) {
int pos = context.cursorPosition;
// Find containing method
MethodInfo containingMethod = document.findContainingMethod(pos);
// Add local variables
if (containingMethod != null) {
List<FieldInfo> available = document.getAvailableVariablesAt(pos);
Set<String> seenNames = new HashSet<>();
for (FieldInfo field : available) {
if (!(field.isLocal() || field.isParameter())) {
continue;
}
if (seenNames.add(field.getName())) {
items.add(AutocompleteItem.fromField(field));
}
}
}
// Add global fields
for (FieldInfo globalField : document.getGlobalFields().values()) {
if (globalField.isVisibleAt(pos)) {
items.add(AutocompleteItem.fromField(globalField));
}
}
// Add enclosing type fields (find via script types map)
ScriptTypeInfo enclosingType = findEnclosingType(pos);
if (enclosingType != null) {
for (FieldInfo field : enclosingType.getFields().values()) {
if (field.isVisibleAt(pos)) {
items.add(AutocompleteItem.fromField(field));
}
}
// Add methods from enclosing type
for (List<MethodInfo> overloads : enclosingType.getMethods().values()) {
for (MethodInfo method : overloads) {
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
}
// Add inner classes of the enclosing type as type suggestions
for (ScriptTypeInfo inner : enclosingType.getInnerClasses()) {
items.add(AutocompleteItem.fromType(inner));
}
// Walk up the outerClass chain to include outer class members and sibling types.
// Inside a nested class, users can access outer class fields/methods/sibling types.
ScriptTypeInfo outer = enclosingType.getOuterClass();
while (outer != null) {
for (ScriptTypeInfo sibling : outer.getInnerClasses()) {
if (sibling != enclosingType) {
items.add(AutocompleteItem.fromType(sibling));
}
}
for (FieldInfo field : outer.getFields().values()) {
items.add(AutocompleteItem.fromField(field));
}
for (List<MethodInfo> overloads : outer.getMethods().values()) {
for (MethodInfo method : overloads) {
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
}
outer = outer.getOuterClass();
}
}
// Add script-defined methods
for (MethodInfo method : document.getAllMethods()) {
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
}
addLanguageUniqueSuggestions(context, items);
// Add keywords
addKeywords(items);
}
protected void addLanguageUniqueSuggestions(Context context, List<AutocompleteItem> items) {
// Add imported types
for (TypeInfo type : document.getImportedTypes()) {
items.add(AutocompleteItem.fromType(type));
}
// Add script-defined types
for (ScriptTypeInfo scriptType : document.getScriptTypesMap().values()) {
items.add(AutocompleteItem.fromType(scriptType));
}
// Add unimported classes that match the prefix (for auto-import)
if (context.prefix != null && context.prefix.length() >= 2 && Character.isUpperCase(context.prefix.charAt(0))) {
addUnimportedClassSuggestions(context.prefix, items);
}
}
/**
* Add suggestions for unimported classes that match the prefix.
* These will trigger auto-import when selected.
*/
protected void addUnimportedClassSuggestions(String prefix, List<AutocompleteItem> items) {
// Get the type resolver
TypeResolver resolver = TypeResolver.getInstance();
// Find classes matching this prefix (not just exact matches)
List<String> matchingClasses = resolver.findClassesByPrefix(prefix, -1);
// Track what's already imported to avoid duplicates
Set<String> importedFullNames = new HashSet<>();
for (TypeInfo imported : document.getImportedTypes()) {
importedFullNames.add(imported.getFullName());
}
// Also track simple names already in the list to avoid showing both
// imported and unimported versions of the same class
Set<String> existingSimpleNames = new HashSet<>();
for (AutocompleteItem item : items) {
if (item.getKind() == AutocompleteItem.Kind.CLASS ||
item.getKind() == AutocompleteItem.Kind.ENUM) {
existingSimpleNames.add(item.getName());
}
}
for (String fullName : matchingClasses) {
// Skip if already imported
if (importedFullNames.contains(fullName)) {
continue;
}
TypeInfo type = resolver.resolveFullName(fullName);
if (type != null && type.isResolved()) {
// Skip if a class with this simple name is already in the list
if (existingSimpleNames.contains(type.getSimpleName())) {
continue;
}
// Create an item that requires import
AutocompleteItem item = new AutocompleteItem.Builder()
.name(type.getSimpleName())
.insertText(type.getSimpleName())
.kind(type.getKind() == TypeInfo.Kind.ENUM ?
AutocompleteItem.Kind.ENUM : AutocompleteItem.Kind.CLASS)
.typeLabel(type.getPackageName())
.signature(type.getFullName())
.sourceData(type)
.requiresImport(true)
.importPath(fullName)
.build();
items.add(item);
existingSimpleNames.add(type.getSimpleName());
}
}
}
/**
* Add Java keywords.
*/
protected void addKeywords(List<AutocompleteItem> items) {
for (String keyword : getKeywords()) {
items.add(AutocompleteItem.keyword(keyword));
}
}
public String[] getKeywords() {
return TypeChecker.getJavaKeywords();
}
/**
* Find the enclosing script type at a position.
* Delegates to ScriptDocument's recursive descent implementation
* which correctly returns the innermost enclosing type for nested classes.
*/
protected ScriptTypeInfo findEnclosingType(int position) {
return document.findEnclosingScriptType(position);
}
/**
* Check if the receiver expression represents static access (class type).
* Returns true if the expression is a TYPE NAME (not a variable).
*/
protected boolean isStaticAccess(String receiverExpr, int position) {
// Use unified static access checker
return TypeResolver.isStaticAccessExpression(receiverExpr, position, document);
}
protected UsageTracker getUsageTracker() {
return UsageTracker.getJavaInstance();
}
/**
* Filter items by prefix, calculate match scores, apply usage boosts, and penalize static members in instance contexts.
*/
protected void filterAndScore(List<AutocompleteItem> items, String prefix,
boolean isMemberAccess, boolean isStaticContext, String ownerFullName) {
UsageTracker tracker = getUsageTracker();
if (prefix == null || prefix.isEmpty()) {
// No filtering needed, all items get a base score + usage boost
for (AutocompleteItem item : items) {
item.calculateMatchScore("", false);
applyUsageBoost(item, tracker, ownerFullName);
applyStaticPenalty(item, isMemberAccess, isStaticContext);
applyObjectMethodPenalty(item);
applyKeywordPenalty(item, prefix);
}
return;
}
// For non-member access (first word), require strict prefix matching
// For member access (after dot), allow fuzzy/contains matching
boolean requirePrefix = !isMemberAccess;
// Filter, score, apply usage boosts, and apply penalties
Iterator<AutocompleteItem> iter = items.iterator();
while (iter.hasNext()) {
AutocompleteItem item = iter.next();
int score = item.calculateMatchScore(prefix, requirePrefix);
if (score < 0) {
iter.remove();
} else {
applyUsageBoost(item, tracker, ownerFullName);
applyStaticPenalty(item, isMemberAccess, isStaticContext);
applyObjectMethodPenalty(item);
applyKeywordPenalty(item, prefix);
}
}
}
/**
* Apply usage-based score boost to an item.
*/
protected void applyUsageBoost(AutocompleteItem item, UsageTracker tracker, String ownerFullName) {
int usageCount = tracker.getUsageCount(item, ownerFullName);
int boost = UsageTracker.calculateUsageBoost(usageCount);
item.addScoreBoost(boost);
}
/**
* Apply penalty to static members when accessed in a non-static (instance) context.
* This matches IntelliJ's behavior where static members are deprioritized when
* accessing through an instance (e.g., Minecraft.getMinecraft().getMinecraft()).
* However, if the item is a very strong match (exact prefix), don't penalize as much.
*/
protected void applyStaticPenalty(AutocompleteItem item, boolean isMemberAccess, boolean isStaticContext) {
// Only apply penalty in member access contexts (after dot)
if (!isMemberAccess) {
return;
}
// Only penalize when we're in an instance context (not static)
if (isStaticContext) {
return;
}
boolean isStatic = item.isStatic();
// Apply penalty to static members in instance context
if (isStatic) {
int matchScore = item.getMatchScore();
// Strong prefix matches (score >= 800) get a lighter penalty
// Weaker matches get pushed down more aggressively
if (matchScore >= 800) {
// Light penalty for exact prefix matches - just deprioritize slightly
item.addScoreBoost(-200);
} else {
// Heavy penalty for fuzzy/substring matches - push to bottom
item.addScoreBoost(-matchScore);
}
}
}
/**
* Apply penalty to inherited Object methods to push them to bottom.
* Strong matches get lighter penalty, weak matches get pushed all the way down.
*/
protected void applyObjectMethodPenalty(AutocompleteItem item) {
if (!item.isInheritedObjectMethod()) {
return;
}
int matchScore = item.getMatchScore();
// Strong prefix matches (score >= 900) get a moderate penalty
// Everything else gets pushed to the very bottom
if (matchScore >= 900) {
// Strong match - moderate penalty to keep it visible but below normal methods
item.addScoreBoost(-500);
} else {
// Weak match - heavy penalty to push to bottom
item.addScoreBoost(-10000);
}
}
/**
* Push keywords below non-keywords unless user typed a longer prefix.
*/
protected void applyKeywordPenalty(AutocompleteItem item, String prefix) {
if (item.getKind() != AutocompleteItem.Kind.KEYWORD) {
return;
}
if (prefix == null || prefix.length() < 2) {
item.addScoreBoost(-10000);
}
}
/**
* Builder class for AutocompleteItem to handle cases without source data.
*/
private static class AutocompleteItemBuilder {
// This would be needed if AutocompleteItem had a builder pattern
}
}
@@ -0,0 +1,284 @@
package noppes.npcs.client.gui.util.script.autocomplete;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import noppes.npcs.CustomNpcs;
import java.io.*;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Tracks user autocomplete selections to improve suggestion ranking.
* Usage counts are persisted to JSON and used to boost frequently selected items.
*
* Key format: "ownerFullName|memberName|KIND"
* - ownerFullName: Full class name for members, empty for standalone items
* - memberName: The item name (method, field, type, etc.)
* - KIND: The item kind (METHOD, FIELD, CLASS, ENUM, etc.)
*
* Examples:
* - "net.minecraft.client.Minecraft|getMinecraft|METHOD"
* - "net.minecraft.entity.player.EntityPlayer|inventory|FIELD"
* - "|EntityPlayer|CLASS" (standalone type suggestion)
* - "|if|KEYWORD"
*/
public class UsageTracker {
private static final String JAVA_FILE = "java_usages.json";
private static final String JS_FILE = "js_usages.json";
private static final long SAVE_INTERVAL_MS = 60_000; // Auto-save every 60 seconds
private static final int USAGE_SCORE_MULTIPLIER = 50; // Score boost per usage
private static final int MAX_USAGE_BOOST = 5000; // Cap the boost to prevent runaway scores
private static UsageTracker javaInstance;
private static UsageTracker jsInstance;
private static boolean initialized = false;
private final Map<String, Integer> usageCounts = new ConcurrentHashMap<>();
private final File file;
private final AtomicBoolean dirty = new AtomicBoolean(false);
private final AtomicBoolean loaded = new AtomicBoolean(false);
private static ScheduledExecutorService scheduler;
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
// ==================== SINGLETON ACCESS ====================
public static synchronized UsageTracker getJavaInstance() {
ensureInitialized();
if (javaInstance == null) {
javaInstance = new UsageTracker(JAVA_FILE);
javaInstance.load();
}
return javaInstance;
}
public static synchronized UsageTracker getJSInstance() {
ensureInitialized();
if (jsInstance == null) {
jsInstance = new UsageTracker(JS_FILE);
jsInstance.load();
}
return jsInstance;
}
/**
* Ensure the tracker system is initialized with auto-save and shutdown hook.
*/
private static void ensureInitialized() {
if (!initialized) {
initialized = true;
initialize();
// Add shutdown hook to save on JVM exit
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
shutdown();
}, "UsageTracker-Shutdown"));
}
}
/**
* Initialize the auto-save scheduler. Call once on client startup.
*/
public static void initialize() {
if (scheduler == null || scheduler.isShutdown()) {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "UsageTracker-AutoSave");
t.setDaemon(true);
return t;
});
scheduler.scheduleAtFixedRate(() -> {
saveAllIfDirty();
}, SAVE_INTERVAL_MS, SAVE_INTERVAL_MS, TimeUnit.MILLISECONDS);
}
}
/**
* Shutdown and save all data. Call on client shutdown.
*/
public static void shutdown() {
saveAllIfDirty();
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
}
}
private static void saveAllIfDirty() {
if (javaInstance != null) {
javaInstance.saveIfDirty();
}
if (jsInstance != null) {
jsInstance.saveIfDirty();
}
}
// ==================== INSTANCE METHODS ====================
private UsageTracker(String filename) {
File dir = getDir();
this.file = new File(dir, filename);
}
private File getDir() {
File dir = new File(CustomNpcs.Dir, "tracked_usages");
if (!dir.exists()) {
dir.mkdir();
}
return dir;
}
/**
* Record that the user selected an autocomplete item.
*
* @param owner The owner context (full class name for members, null/empty for standalone)
* @param name The item name
* @param kind The item kind
*/
public void recordUsage(String owner, String name, AutocompleteItem.Kind kind) {
String key = buildKey(owner, name, kind);
usageCounts.merge(key, 1, Integer::sum);
dirty.set(true);
}
/**
* Record usage directly from an AutocompleteItem.
*
* @param item The selected autocomplete item
* @param ownerFullName The full class name of the owner (for member access), or null
*/
public void recordUsage(AutocompleteItem item, String ownerFullName) {
String name = item.getSearchName() != null ? item.getSearchName() : item.getName();
// For methods, include parameter count to distinguish overloads
if (item.getKind() == AutocompleteItem.Kind.METHOD) {
int paramCount = item.getParameterCount();
name = name + "(" + paramCount + ")";
}
recordUsage(ownerFullName, name, item.getKind());
}
/**
* Get the usage count for an item.
*
* @param owner The owner context
* @param name The item name
* @param kind The item kind
* @return The number of times this item was selected
*/
public int getUsageCount(String owner, String name, AutocompleteItem.Kind kind) {
String key = buildKey(owner, name, kind);
return usageCounts.getOrDefault(key, 0);
}
/**
* Get usage count directly from an AutocompleteItem.
*/
public int getUsageCount(AutocompleteItem item, String ownerFullName) {
String name = item.getSearchName() != null ? item.getSearchName() : item.getName();
// For methods, include parameter count to distinguish overloads
if (item.getKind() == AutocompleteItem.Kind.METHOD) {
int paramCount = item.getParameterCount();
name = name + "(" + paramCount + ")";
}
return getUsageCount(ownerFullName, name, item.getKind());
}
/**
* Calculate the score boost based on usage count.
* Uses a logarithmic scale to prevent very frequent items from dominating completely.
*
* @param usageCount The number of times the item was selected
* @return The score boost to add
*/
public static int calculateUsageBoost(int usageCount) {
if (usageCount <= 0) return 0;
// Logarithmic scaling: log2(count + 1) * multiplier
// This gives diminishing returns for very high counts
int boost = (int) (Math.log(usageCount + 1) / Math.log(2) * USAGE_SCORE_MULTIPLIER);
return Math.min(boost, MAX_USAGE_BOOST);
}
/**
* Build a unique key for an item.
*/
public static String buildKey(String owner, String name, AutocompleteItem.Kind kind) {
String ownerPart = owner != null ? owner : "";
String kindPart = kind != null ? kind.name() : "UNKNOWN";
return ownerPart + "|" + name + "|" + kindPart;
}
// ==================== PERSISTENCE ====================
/**
* Load usage data from file.
*/
public void load() {
if (loaded.get()) return;
if (!file.exists()) {
loaded.set(true);
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
Type type = new TypeToken<Map<String, Integer>>(){}.getType();
Map<String, Integer> data = GSON.fromJson(reader, type);
if (data != null) {
usageCounts.putAll(data);
}
loaded.set(true);
} catch (Exception e) {
System.err.println("[UsageTracker] Failed to load " + file.getName() + ": " + e.getMessage());
loaded.set(true); // Mark as loaded even on failure to prevent retry loops
}
}
/**
* Save usage data to file if there are pending changes.
*/
public void saveIfDirty() {
if (!dirty.compareAndSet(true, false)) {
return; // Not dirty, nothing to save
}
save();
}
/**
* Force save usage data to file.
*/
public void save() {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
GSON.toJson(usageCounts, writer);
} catch (Exception e) {
System.err.println("[UsageTracker] Failed to save " + file.getName() + ": " + e.getMessage());
dirty.set(true); // Mark dirty again so we retry later
}
}
/**
* Clear all usage data (for testing/reset purposes).
*/
public void clear() {
usageCounts.clear();
dirty.set(true);
}
/**
* Get the number of tracked items.
*/
public int size() {
return usageCounts.size();
}
}
@@ -0,0 +1,323 @@
package noppes.npcs.client.gui.util.script.interpreter;
/**
* Utility class for parsing and analyzing code text.
* Provides methods for finding matching braces/parentheses, removing comments/strings,
* and locating keywords in code.
*/
public final class CodeParser {
private CodeParser() {} // Utility class
// ==================== Matching Delimiters ====================
/**
* Find the closing brace that matches the opening brace at the given position.
* @param text The text to search
* @param openBrace Position of the opening '{'
* @return Position of matching '}' or -1 if not found
*/
public static int findMatchingBrace(String text, int openBrace) {
if (openBrace < 0 || openBrace >= text.length() || text.charAt(openBrace) != '{') {
return -1;
}
int depth = 1;
for (int i = openBrace + 1; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '{') depth++;
else if (c == '}') {
depth--;
if (depth == 0) return i;
}
}
return -1;
}
/**
* Find the closing parenthesis that matches the opening parenthesis at the given position.
* @param text The text to search
* @param openParen Position of the opening '('
* @return Position of matching ')' or -1 if not found
*/
public static int findMatchingParen(String text, int openParen) {
if (openParen < 0 || openParen >= text.length() || text.charAt(openParen) != '(') {
return -1;
}
int depth = 1;
for (int i = openParen + 1; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '(') depth++;
else if (c == ')') {
depth--;
if (depth == 0) return i;
}
}
return -1;
}
// ==================== Comment/String Removal ====================
/**
* Remove string literals and comments from code for structural analysis.
* This is used when analyzing control flow to avoid false positives from
* keywords inside strings or comments.
* @param code The source code
* @return Code with strings and comments removed
*/
public static String removeStringsAndComments(String code) {
StringBuilder result = new StringBuilder();
boolean inString = false;
boolean inChar = false;
boolean inLineComment = false;
boolean inBlockComment = false;
for (int i = 0; i < code.length(); i++) {
char c = code.charAt(i);
char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0;
if (inLineComment) {
if (c == '\n') {
inLineComment = false;
result.append(c);
}
continue;
}
if (inBlockComment) {
if (c == '*' && next == '/') {
inBlockComment = false;
i++; // skip '/'
}
continue;
}
if (inString || inChar) {
if (c == '\\' && i + 1 < code.length()) {
i++; // skip escaped character
continue;
}
if ((inString && c == '"') || (inChar && c == '\'')) {
inString = false;
inChar = false;
}
continue;
}
// Check for start of comments or strings
if (c == '/' && next == '/') {
inLineComment = true;
i++;
continue;
}
if (c == '/' && next == '*') {
inBlockComment = true;
i++;
continue;
}
if (c == '"') {
inString = true;
continue;
}
if (c == '\'') {
inChar = true;
continue;
}
result.append(c);
}
return result.toString();
}
/**
* Remove only comments from code, keeping strings intact and preserving positions.
* Comments are replaced with spaces to maintain character positions.
* @param code The source code
* @return Code with comments replaced by spaces
*/
public static String removeComments(String code) {
StringBuilder result = new StringBuilder();
boolean inString = false;
boolean inChar = false;
boolean inLineComment = false;
boolean inBlockComment = false;
for (int i = 0; i < code.length(); i++) {
char c = code.charAt(i);
char next = (i + 1 < code.length()) ? code.charAt(i + 1) : 0;
if (inLineComment) {
if (c == '\n') {
inLineComment = false;
result.append(c);
} else {
result.append(' '); // Preserve position with spaces
}
continue;
}
if (inBlockComment) {
if (c == '*' && next == '/') {
inBlockComment = false;
result.append(" "); // Preserve position
i++;
} else {
result.append(c == '\n' ? '\n' : ' '); // Preserve newlines
}
continue;
}
if (inString) {
result.append(c);
if (c == '\\' && i + 1 < code.length()) {
result.append(code.charAt(++i));
continue;
}
if (c == '"') inString = false;
continue;
}
if (inChar) {
result.append(c);
if (c == '\\' && i + 1 < code.length()) {
result.append(code.charAt(++i));
continue;
}
if (c == '\'') inChar = false;
continue;
}
// Check for start of comments
if (c == '/' && next == '/') {
inLineComment = true;
result.append(" "); // Preserve position
i++;
continue;
}
if (c == '/' && next == '*') {
inBlockComment = true;
result.append(" "); // Preserve position
i++;
continue;
}
if (c == '"') { inString = true; }
if (c == '\'') { inChar = true; }
result.append(c);
}
return result.toString();
}
// ==================== Keyword Detection ====================
/**
* Check if a keyword exists at the given position in text.
* Verifies the keyword is not part of a larger identifier.
* @param text The text to check
* @param pos Position to check
* @param keyword The keyword to look for
* @return true if the keyword is at this position as a standalone word
*/
public static boolean isKeywordAt(String text, int pos, String keyword) {
if (pos + keyword.length() > text.length()) return false;
if (!text.substring(pos).startsWith(keyword)) return false;
boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1));
boolean validAfter = pos + keyword.length() >= text.length()
|| !Character.isLetterOrDigit(text.charAt(pos + keyword.length()));
return validBefore && validAfter;
}
/**
* Find the next "return" keyword that is not inside a string literal.
* @param text The text to search
* @param start Position to start searching from
* @return Position of "return" or -1 if not found
*/
public static int findReturnKeyword(String text, int start) {
boolean inString = false;
boolean inChar = false;
for (int i = start; i < text.length() - 5; i++) {
char c = text.charAt(i);
if (inString) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '"') inString = false;
continue;
}
if (inChar) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '\'') inChar = false;
continue;
}
if (c == '"') { inString = true; continue; }
if (c == '\'') { inChar = true; continue; }
// Check for "return" keyword
if (isKeywordAt(text, i, "return")) {
return i;
}
}
return -1;
}
/**
* Find the semicolon that ends a return statement, handling nested structures.
* @param text The text to search
* @param start Position to start searching (after "return" keyword)
* @return Position of ';' or -1 if not found
*/
public static int findReturnSemicolon(String text, int start) {
int parenDepth = 0;
int braceDepth = 0;
boolean inString = false;
boolean inChar = false;
for (int i = start; i < text.length(); i++) {
char c = text.charAt(i);
if (inString) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '"') inString = false;
continue;
}
if (inChar) {
if (c == '\\' && i + 1 < text.length()) { i++; continue; }
if (c == '\'') inChar = false;
continue;
}
if (c == '"') { inString = true; continue; }
if (c == '\'') { inChar = true; continue; }
if (c == '(') { parenDepth++; continue; }
if (c == ')') { parenDepth--; continue; }
if (c == '{') { braceDepth++; continue; }
if (c == '}') { braceDepth--; continue; }
if (c == ';' && parenDepth == 0 && braceDepth == 0) {
return i;
}
}
return -1;
}
// ==================== Whitespace Utilities ====================
/**
* Skip whitespace characters starting from the given position.
* @param text The text
* @param pos Starting position
* @return Position of first non-whitespace character
*/
public static int skipWhitespace(String text, int pos) {
while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) {
pos++;
}
return pos;
}
}
@@ -0,0 +1,215 @@
package noppes.npcs.client.gui.util.script.interpreter;
/**
* Analyzes control flow in method bodies to determine if all paths return a value.
* Used for detecting "missing return statement" errors.
*/
public final class ControlFlowAnalyzer {
private ControlFlowAnalyzer() {} // Utility class
/**
* Check if the method body has a guaranteed return on all code paths.
*/
public static boolean hasGuaranteedReturn(String body) {
String cleanBody = CodeParser.removeStringsAndComments(body);
return hasTopLevelReturn(cleanBody)
|| hasCompleteIfElseReturn(cleanBody)
|| hasTryFinallyReturn(cleanBody)
|| hasCompleteSwitchReturn(cleanBody);
}
/**
* Check for a bare return statement at the method's top level (brace depth 0).
*/
private static boolean hasTopLevelReturn(String body) {
int braceDepth = 0;
int parenDepth = 0;
for (int i = 0; i < body.length(); i++) {
char c = body.charAt(i);
if (c == '{') braceDepth++;
else if (c == '}') braceDepth--;
else if (c == '(') parenDepth++;
else if (c == ')') parenDepth--;
if (braceDepth == 0 && parenDepth == 0 && isKeywordAt(body, i, "return")) {
return true;
}
}
return false;
}
/**
* Check for complete if-else chains where all branches return.
*/
private static boolean hasCompleteIfElseReturn(String body) {
int braceDepth = 0;
for (int i = 0; i < body.length(); i++) {
char c = body.charAt(i);
if (c == '{') { braceDepth++; continue; }
if (c == '}') { braceDepth--; continue; }
if (braceDepth == 0 && isKeywordAt(body, i, "if")) {
if (checkIfElseChainReturns(body, i)) {
return true;
}
}
}
return false;
}
/**
* Check if an if-else chain starting at the given position guarantees a return.
* Handles: if(true) { return; } - always returns (else unreachable)
* if(false) { } else { return; } - else always executes
*/
private static boolean checkIfElseChainReturns(String body, int ifStart) {
int pos = ifStart;
while (pos < body.length()) {
if (!body.substring(pos).startsWith("if")) return false;
pos += 2;
pos = skipWhitespace(body, pos);
if (pos >= body.length() || body.charAt(pos) != '(') return false;
int condStart = pos;
int condEnd = CodeParser.findMatchingParen(body, pos);
if (condEnd < 0) return false;
// Check for literal true/false conditions
String condition = body.substring(condStart + 1, condEnd).trim();
boolean isAlwaysTrue = condition.equals("true");
boolean isAlwaysFalse = condition.equals("false");
pos = skipWhitespace(body, condEnd + 1);
if (pos >= body.length() || body.charAt(pos) != '{') return false;
int blockEnd = CodeParser.findMatchingBrace(body, pos);
if (blockEnd < 0) return false;
String ifBlock = body.substring(pos + 1, blockEnd);
boolean ifBlockReturns = hasGuaranteedReturn(ifBlock);
// if(true) with return means we always return (else is unreachable)
if (isAlwaysTrue && ifBlockReturns) {
return true;
}
pos = skipWhitespace(body, blockEnd + 1);
// Must have else clause for guaranteed return
if (!body.substring(pos).startsWith("else")) {
return false;
}
pos += 4;
pos = skipWhitespace(body, pos);
// else if - continue the chain
if (isKeywordAt(body, pos, "if")) {
continue;
}
// Plain else block - final branch
if (pos >= body.length() || body.charAt(pos) != '{') return false;
int elseBlockEnd = CodeParser.findMatchingBrace(body, pos);
if (elseBlockEnd < 0) return false;
String elseBlock = body.substring(pos + 1, elseBlockEnd);
boolean elseBlockReturns = hasGuaranteedReturn(elseBlock);
// For if-else to guarantee return: both branches must return
// Special case: if(false) means if-block never executes, so only else matters
if (isAlwaysFalse) {
return elseBlockReturns;
}
// Normal case: both if and else must return
return ifBlockReturns && elseBlockReturns;
}
return false;
}
/**
* Check for try-finally where finally block returns.
*/
private static boolean hasTryFinallyReturn(String body) {
for (int i = 0; i < body.length(); i++) {
if (!isKeywordAt(body, i, "try")) continue;
int tryBlockStart = body.indexOf('{', i);
if (tryBlockStart < 0) continue;
int tryBlockEnd = CodeParser.findMatchingBrace(body, tryBlockStart);
if (tryBlockEnd < 0) continue;
// Skip catch blocks
int searchPos = skipCatchBlocks(body, tryBlockEnd + 1);
searchPos = skipWhitespace(body, searchPos);
// Check for finally
if (body.substring(searchPos).startsWith("finally")) {
int finallyBlockStart = body.indexOf('{', searchPos);
if (finallyBlockStart >= 0) {
int finallyBlockEnd = CodeParser.findMatchingBrace(body, finallyBlockStart);
if (finallyBlockEnd >= 0) {
String finallyBlock = body.substring(finallyBlockStart + 1, finallyBlockEnd);
if (hasGuaranteedReturn(finallyBlock)) {
return true;
}
}
}
}
}
return false;
}
private static int skipCatchBlocks(String body, int start) {
int pos = start;
while (true) {
pos = skipWhitespace(body, pos);
if (pos >= body.length() || !body.substring(pos).startsWith("catch")) break;
int catchBlockStart = body.indexOf('{', pos);
if (catchBlockStart < 0) break;
int catchBlockEnd = CodeParser.findMatchingBrace(body, catchBlockStart);
if (catchBlockEnd < 0) break;
pos = catchBlockEnd + 1;
}
return pos;
}
/**
* Check for switch with default where all cases return.
* Returns false for now - full implementation is complex.
*/
private static boolean hasCompleteSwitchReturn(String body) {
return false;
}
// ==================== Utility Methods ====================
private static boolean isKeywordAt(String text, int pos, String keyword) {
if (pos + keyword.length() > text.length()) return false;
if (!text.substring(pos).startsWith(keyword)) return false;
boolean validBefore = pos == 0 || !Character.isLetterOrDigit(text.charAt(pos - 1));
boolean validAfter = pos + keyword.length() >= text.length()
|| !Character.isLetterOrDigit(text.charAt(pos + keyword.length()));
return validBefore && validAfter;
}
private static int skipWhitespace(String text, int pos) {
while (pos < text.length() && Character.isWhitespace(text.charAt(pos))) {
pos++;
}
return pos;
}
}
@@ -0,0 +1,292 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.ClientProxy;
import noppes.npcs.client.gui.util.script.interpreter.field.AssignmentInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo;
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.getAllMethods()) {
//!call.isConstructor for enum declarations
if (!call.isConstructor() && 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);
} else if (call.hasArgTypeError()) {
// Handle return type mismatch (underline the method name) - currently commented out
// if (call.hasReturnTypeMismatch()) { ... }
// Handle arg type errors (underline specific arguments)
for (MethodCallInfo.ArgumentTypeError error : call.getArgumentTypeErrors()) {
MethodCallInfo.Argument arg = error.getArg();
drawUnderlineForSpan(arg.getStartOffset(), arg.getEndOffset(),
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
} else if(call.hasError()){
// Handle other call errors (underline the whole call)
drawUnderlineForSpan(call.getMethodNameStart(), call.getCloseParenOffset() + 1,
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
}
// 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.getAllMethods()) {
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)
else if (method.hasMissingReturnError()) {
int methodNameStart = method.getNameOffset();
int methodNameEnd = methodNameStart + method.getName().length();
drawUnderlineForSpan(methodNameStart, methodNameEnd, lineStartX, baselineY,
lineText, lineStart, lineEnd, ERROR_COLOR);
}
// Handle parameter errors
else 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);
}
}
// Handle all other errors
else if (method.hasError()) {
drawUnderlineForSpan(method.getFullDeclarationOffset(), method.getDeclarationEnd(), lineStartX,
baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
}
}
for(ScriptTypeInfo types : doc.getScriptTypes()){
if(!types.hasError())
continue;
int typeStart = types.getDeclarationOffset();
int typeEnd = types.getBodyStart();
drawUnderlineForSpan(typeStart, typeEnd, lineStartX, baselineY,
lineText, lineStart, lineEnd, ERROR_COLOR);
}
}
/**
* 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();
}
}
@@ -0,0 +1,517 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Helper class for marking chained field accesses in script documents.
* Handles patterns like: identifier.field, this.field, super.field, method().field
*/
public class FieldChainMarker {
private final ScriptDocument document;
private final String text;
public FieldChainMarker(ScriptDocument document, String text) {
this.document = document;
this.text = text;
}
/**
* Mark all chained field accesses in the document.
* Uses two passes: one for standard chains, one for method/array result chains.
*/
public void markChainedFieldAccesses(List<ScriptLine.Mark> marks) {
// ==================== PASS 1: Standard identifier chains ====================
// Pattern: identifier.identifier, this.identifier, super.identifier
Pattern chainPattern = Pattern.compile(
"\\b(this|super|[a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
Matcher m = chainPattern.matcher(text);
while (m.find()) {
int chainStart = m.start(1);
// Skip excluded ranges and imports
if (document.isExcluded(chainStart) || document.isInImportOrPackage(chainStart)) continue;
// Skip if second segment is a method call - markMethodCalls handles it
if (document.isFollowedByParen(m.end(2))) continue;
// Build the full chain
ChainData chain = buildChain(m);
if (chain == null) continue;
// Resolve the starting type
ChainContext ctx = resolveChainStart(chain, chainStart);
// Mark each segment
for (int i = ctx.startIndex; i < chain.segments.size(); i++) {
ctx.currentIndex = i;
int[] pos = chain.positions.get(i);
if (document.isExcluded(pos[0])) continue;
// Determine what type of marking this segment needs
MarkResult result = resolveSegmentMark(ctx, chainStart);
// Apply the mark
if (result.mark != null) {
marks.add(result.mark);
}
// Update context for next segment
ctx.currentType = result.nextType;
}
}
// ==================== PASS 2: Method/array result chains ====================
// Handle: getMinecraft().thePlayer, array[0].length
Pattern dotIdent = Pattern.compile("\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)");
Matcher md = dotIdent.matcher(text);
while (md.find()) {
int identStart = md.start(1);
int identEnd = md.end(1);
int dotPos = md.start();
// Skip excluded ranges and imports
if (document.isExcluded(identStart) || document.isInImportOrPackage(identStart)) continue;
// Only handle when preceded by ')' or ']' (method call or array access result)
Character precedingChar = getNonWhitespaceBefore(dotPos);
if (precedingChar == null || (precedingChar != ')' && precedingChar != ']')) continue;
// Get receiver type from the expression before the dot
int[] bounds = document.findReceiverBoundsBefore(dotPos);
if (bounds == null) continue;
if (document.isExcluded(bounds[0]) || document.isInImportOrPackage(bounds[0])) continue;
String receiverExpr = text.substring(bounds[0], bounds[1]).trim();
if (receiverExpr.isEmpty()) continue;
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, bounds[0]);
// Mark the first field and continue the chain
markReceiverChainSegments(marks, md.group(1), identStart, identEnd, receiverType);
}
}
// ==================== CHAIN BUILDING HELPERS ====================
/** Data class for a parsed chain */
private static class ChainData {
final List<String> segments = new ArrayList<>();
final List<int[]> positions = new ArrayList<>();
}
/** Context for chain resolution */
private static class ChainContext {
ChainData chain;
int currentIndex;
TypeInfo currentType;
ScriptTypeInfo enclosingType; // The enclosing script type for 'this' resolution
int startIndex;
boolean firstIsThis;
boolean firstIsSuper;
boolean firstIsPrecededByDot;
}
/** Result of resolving a segment's mark */
private static class MarkResult {
final ScriptLine.Mark mark;
final TypeInfo nextType;
MarkResult(ScriptLine.Mark mark, TypeInfo nextType) {
this.mark = mark;
this.nextType = nextType;
}
}
/** Build a complete chain from a regex match, continuing to read more segments */
private ChainData buildChain(Matcher m) {
ChainData chain = new ChainData();
// Add first two segments from the match
chain.segments.add(m.group(1));
chain.positions.add(new int[]{m.start(1), m.end(1)});
chain.segments.add(m.group(2));
chain.positions.add(new int[]{m.start(2), m.end(2)});
// Continue reading subsequent .identifier segments
int pos = m.end(2);
while (pos < text.length()) {
pos = document.skipWhitespace(pos);
if (pos >= text.length() || text.charAt(pos) != '.') break;
pos++; // Skip dot
pos = document.skipWhitespace(pos);
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
// Read identifier
int identStart = pos;
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++;
int identEnd = pos;
// Stop if this is a method call
if (document.isFollowedByParen(identEnd)) break;
chain.segments.add(text.substring(identStart, identEnd));
chain.positions.add(new int[]{identStart, identEnd});
}
return chain;
}
/** Resolve the starting context for a chain */
private ChainContext resolveChainStart(ChainData chain, int chainStart) {
ChainContext ctx = new ChainContext();
ctx.chain = chain;
ctx.currentIndex = 0;
String first = chain.segments.get(0);
ctx.firstIsThis = first.equals("this");
ctx.firstIsSuper = first.equals("super");
ctx.firstIsPrecededByDot = document.isPrecededByDot(chainStart);
ctx.startIndex = ctx.firstIsPrecededByDot ? 0 : 1;
ctx.currentType = null;
ctx.enclosingType = document.findEnclosingScriptType(chainStart);
if (ctx.firstIsThis) {
// 'this' - resolve to enclosing class/global fields
ctx.currentType = document.resolveThisType(chainStart);
if (ctx.currentType == null) {
ctx.currentType = ctx.enclosingType;
}
} else if (ctx.firstIsSuper) {
// 'super' - resolve to parent class
if (ctx.enclosingType != null && ctx.enclosingType.hasSuperClass()) {
ctx.currentType = ctx.enclosingType.getSuperClass();
}
} else if (ctx.firstIsPrecededByDot) {
// Field access on a receiver (e.g., getMinecraft().thePlayer)
TypeInfo receiverType = document.resolveReceiverChain(chainStart);
if (receiverType != null && receiverType.hasField(first)) {
FieldInfo varInfo = receiverType.getFieldInfo(first);
ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null;
}
} else {
// Try as type first (static access), then as variable
TypeInfo typeCheck = document.resolveType(first);
if (typeCheck != null && typeCheck.isResolved()) {
ctx.currentType = typeCheck;
} else {
FieldInfo varInfo = document.resolveVariable(first, chainStart);
ctx.currentType = (varInfo != null) ? varInfo.getTypeInfo() : null;
}
}
return ctx;
}
/** Resolve what mark a segment should get */
private MarkResult resolveSegmentMark(ChainContext ctx, int chainStart) {
int index = ctx.currentIndex;
String segment = ctx.chain.segments.get(index);
int[] pos = ctx.chain.positions.get(index);
// Case 1: First segment preceded by dot (field on receiver)
if (index == 0 && ctx.firstIsPrecededByDot) {
return resolveReceiverFieldMark(ctx, chainStart);
}
// Case 2: this.field
if (index == 1 && ctx.firstIsThis) {
return resolveThisFieldMark(ctx);
}
// Case 3: super.field
if (index == 1 && ctx.firstIsSuper) {
return resolveSuperFieldMark(ctx);
}
// Case 4: Resolved type with field
if (ctx.currentType != null && ctx.currentType.isResolved()) {
return resolveTypedFieldMark(ctx);
}
// Case 5: Unresolved - mark as undefined
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
);
}
// ==================== SEGMENT RESOLUTION HELPERS ====================
/** Resolve mark for field access on a receiver (preceded by dot) */
private MarkResult resolveReceiverFieldMark(ChainContext ctx, int chainStart) {
int index = ctx.currentIndex;
String segment = ctx.chain.segments.get(index);
int[] pos = ctx.chain.positions.get(index);
boolean isLast = (index == ctx.chain.segments.size() - 1);
boolean isStatic = isStaticContext(ctx);
TypeInfo receiverType = document.resolveReceiverChain(chainStart);
if (receiverType != null && receiverType.hasField(segment)) {
FieldInfo fieldInfo = receiverType.getFieldInfo(segment);
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
receiverType, fieldInfo, isLast, isStatic);
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
);
}
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
);
}
/** Resolve mark for this.field access */
private MarkResult resolveThisFieldMark(ChainContext ctx) {
int index = ctx.currentIndex;
String segment = ctx.chain.segments.get(index);
int[] pos = ctx.chain.positions.get(index);
boolean isLast = (index == ctx.chain.segments.size() - 1);
boolean found = false;
FieldInfo fieldInfo = null;
TypeInfo thisType = ctx.currentType != null ? ctx.currentType : ctx.enclosingType;
if (thisType != null && thisType.hasField(segment)) {
found = true;
fieldInfo = thisType.getFieldInfo(segment);
} else if (document.getGlobalFields().containsKey(segment)) {
found = true;
fieldInfo = document.getGlobalFields().get(segment);
}
if (found) {
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1], thisType,
fieldInfo, isLast, false);
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
);
}
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR), null
);
}
/** Resolve mark for super.field access */
private MarkResult resolveSuperFieldMark(ChainContext ctx) {
int index = ctx.currentIndex;
String segment = ctx.chain.segments.get(index);
int[] pos = ctx.chain.positions.get(index);
boolean isLast = (index == ctx.chain.segments.size() - 1);
TypeInfo superType = ctx.currentType;
if (superType == null) {
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR,
TokenErrorMessage.from("Cannot resolve field '" + segment + "'").clearOtherErrors()),
null
);
}
// Search through inheritance hierarchy
boolean found = false;
FieldInfo fieldInfo = null;
if (superType instanceof ScriptTypeInfo) {
ScriptTypeInfo scriptSuper = (ScriptTypeInfo) superType;
found = scriptSuper.hasFieldInHierarchy(segment);
if (found) fieldInfo = scriptSuper.getFieldInfoInHierarchy(segment);
} else {
found = superType.hasField(segment);
if (found) fieldInfo = superType.getFieldInfo(segment);
}
if (found) {
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
superType, fieldInfo, isLast, false);
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
);
}
String errorMsg = "Field '" + segment + "' not found in parent class hierarchy starting from '"
+ superType.getSimpleName() + "'";
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR,
TokenErrorMessage.from(errorMsg).clearOtherErrors()),
null
);
}
/** Resolve mark for typed field access */
private MarkResult resolveTypedFieldMark(ChainContext ctx) {
int index = ctx.currentIndex;
String segment = ctx.chain.segments.get(index);
int[] pos = ctx.chain.positions.get(index);
boolean isLast = (index == ctx.chain.segments.size() - 1);
boolean isStatic = isStaticContext(ctx);
TypeInfo currentType = ctx.currentType;
if (!currentType.hasField(segment)) {
// Field not found — check if the segment names an inner class.
// This handles chains like Outer.Inner where Inner is a nested type, not a field.
if (currentType instanceof ScriptTypeInfo) {
ScriptTypeInfo innerClass = ((ScriptTypeInfo) currentType).getInnerClass(segment);
if (innerClass != null) {
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], innerClass.getTokenType(), innerClass),
innerClass
);
}
}
// Also check Java inner classes via reflection (e.g., Map.Entry)
if (currentType.getJavaClass() != null) {
try {
for (Class<?> nested : currentType.getJavaClass().getDeclaredClasses()) {
if (java.lang.reflect.Modifier.isPublic(nested.getModifiers())
&& nested.getSimpleName().equals(segment)) {
TypeInfo nestedType = TypeInfo.fromClass(nested);
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], nestedType.getTokenType(), nestedType),
nestedType
);
}
}
} catch (SecurityException ignored) { }
}
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR),
null
);
}
FieldInfo fieldInfo = currentType.getFieldInfo(segment);
// Check for static access error
if (isStatic && fieldInfo != null && !fieldInfo.isStatic()) {
TokenErrorMessage errorMsg = TokenErrorMessage
.from("Cannot access non-static field '" + segment + "' from static context '"
+ currentType.getSimpleName() + "'")
.clearOtherErrors();
return new MarkResult(
new ScriptLine.Mark(pos[0], pos[1], TokenType.UNDEFINED_VAR, errorMsg),
null
);
}
// Valid field access
FieldAccessInfo accessInfo = document.createFieldAccessInfo(segment, pos[0], pos[1],
currentType, fieldInfo, isLast, isStatic);
return new MarkResult(new ScriptLine.Mark(pos[0], pos[1], getFieldTokenType(fieldInfo), accessInfo),
(fieldInfo != null) ? fieldInfo.getTypeInfo() : null
);
}
/** Mark segments following a method call or array access result */
private void markReceiverChainSegments(List<ScriptLine.Mark> marks, String firstField,
int identStart, int identEnd, TypeInfo receiverType) {
// Mark the first field
FieldInfo fInfo = null;
TypeInfo currentType = null;
if (receiverType != null && receiverType.hasField(firstField)) {
fInfo = receiverType.getFieldInfo(firstField);
boolean hasMore = document.isFollowedByDot(identEnd);
// For method/array result chains, the receiver is always an instance, not a type
boolean isStatic = false;
FieldAccessInfo accessInfo = document.createFieldAccessInfo(firstField, identStart, identEnd,
receiverType, fInfo, !hasMore, isStatic);
marks.add(new ScriptLine.Mark(identStart, identEnd, getFieldTokenType(fInfo), accessInfo));
currentType = (fInfo != null) ? fInfo.getTypeInfo() : null;
} else {
marks.add(new ScriptLine.Mark(identStart, identEnd, TokenType.UNDEFINED_VAR));
return;
}
// Continue the chain
int pos = identEnd;
while (pos < text.length()) {
pos = document.skipWhitespace(pos);
if (pos >= text.length() || text.charAt(pos) != '.') break;
pos++; // Skip dot
pos = document.skipWhitespace(pos);
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
// Read identifier
int nStart = pos;
while (pos < text.length() && Character.isJavaIdentifierPart(text.charAt(pos))) pos++;
int nEnd = pos;
String seg = text.substring(nStart, nEnd);
// Stop if method call
if (document.isFollowedByParen(nEnd)) break;
if (document.isExcluded(nStart)) break;
boolean isLast = !document.isFollowedByDot(nEnd);
if (currentType != null && currentType.isResolved() && currentType.hasField(seg)) {
FieldInfo segInfo = currentType.getFieldInfo(seg);
// In method/array result chains, segments are always instance access
boolean isStatic = false;
FieldAccessInfo accessInfo = document.createFieldAccessInfo(seg, nStart, nEnd,
currentType, segInfo, isLast, isStatic);
marks.add(new ScriptLine.Mark(nStart, nEnd, getFieldTokenType(segInfo), accessInfo));
currentType = (segInfo != null) ? segInfo.getTypeInfo() : null;
} else {
marks.add(new ScriptLine.Mark(nStart, nEnd, TokenType.UNDEFINED_VAR));
break;
}
}
}
// ==================== UTILITY HELPERS ====================
private TokenType getFieldTokenType(FieldInfo fieldInfo) {
if (fieldInfo != null && fieldInfo.isEnumConstant())
return TokenType.ENUM_CONSTANT;
return TokenType.GLOBAL_FIELD;
}
/** Get the non-whitespace character before a position */
private Character getNonWhitespaceBefore(int pos) {
int before = pos - 1;
while (before >= 0 && Character.isWhitespace(text.charAt(before))) before--;
return (before >= 0) ? text.charAt(before) : null;
}
/** Check if the receiver type is a class/type (static context) */
private boolean isStaticContext(ChainContext ctx) {
// Static context if the previous segment name resolves to a type/class
if (ctx.currentIndex <= 0)
return false;
String previousSegment = ctx.chain.segments.get(ctx.currentIndex - 1);
int prevPos = ctx.chain.positions.get(ctx.currentIndex - 1)[0];
// Use unified static access checker
return TypeResolver.isStaticAccessExpression(previousSegment, prevPos, document);
}
}
@@ -0,0 +1,104 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
/**
* Represents an inner callable scope (lambda or JS function expression).
* These are NOT added to the methods list - they're resolution-only scopes.
*/
public class InnerCallableScope {
public enum Kind {
JAVA_LAMBDA,
JS_FUNCTION_EXPR,
JS_ARROW_FUNC,
JS_SHORTHAND_METHOD
}
private final Kind kind;
private final int headerStart; // Start of "(params) ->" or "function(params)"
private final int headerEnd; // End of header (position of "->" for lambda, ")" for JS func)
private final int bodyStart; // Start of body (expression start or '{')
private final int bodyEnd; // End of body (expression end or '}')
private final List<FieldInfo> parameters = new ArrayList<>();
private final Map<String, FieldInfo> locals = new HashMap<>();
private InnerCallableScope parentScope; // Enclosing inner scope (for nested lambdas)
private TypeInfo expectedType; // The expected functional interface type (set during resolution)
private TypeInfo containingObjectType; // For object-literal methods, the synthetic object-literal type
public InnerCallableScope(Kind kind, int headerStart, int headerEnd, int bodyStart, int bodyEnd) {
this.kind = kind;
this.headerStart = headerStart;
this.headerEnd = headerEnd;
this.bodyStart = bodyStart;
this.bodyEnd = bodyEnd;
}
// Getters
public Kind getKind() { return kind; }
public int getHeaderStart() { return headerStart; }
public int getHeaderEnd() { return headerEnd; }
public int getBodyStart() { return bodyStart; }
public int getBodyEnd() { return bodyEnd; }
public List<FieldInfo> getParameters() { return parameters; }
public Map<String, FieldInfo> getLocals() { return locals; }
public InnerCallableScope getParentScope() { return parentScope; }
public TypeInfo getExpectedType() { return expectedType; }
public TypeInfo getContainingObjectType() { return containingObjectType; }
// Setters
public void setParentScope(InnerCallableScope parent) { this.parentScope = parent; }
public void setExpectedType(TypeInfo type) { this.expectedType = type; }
public void setContainingObjectType(TypeInfo type) { this.containingObjectType = type; }
public void addParameter(FieldInfo param) {
parameters.add(param);
}
public void addLocal(String name, FieldInfo local) {
locals.put(name, local);
}
public FieldInfo getParameter(String name) {
for (FieldInfo param : parameters) {
if (param.getName().equals(name)) {
return param;
}
}
return null;
}
public boolean hasParameter(String name) {
return getParameter(name) != null;
}
/**
* Check if a position is inside this scope's body.
*/
public boolean containsPosition(int position) {
return position >= bodyStart && position < bodyEnd;
}
/**
* Check if a position is inside this scope's header (parameter list).
*/
public boolean containsHeaderPosition(int position) {
return position >= headerStart && position < headerEnd;
}
/**
* Get the full range (header + body).
*/
public int getFullStart() {
return headerStart;
}
public int getFullEnd() {
return bodyEnd;
}
}
@@ -0,0 +1,224 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.gui.util.script.ScopeInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class LoopVariableParser {
private static final Pattern ENHANCED_FOR = Pattern.compile(
"\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*:\\s*([^)]+)\\)");
private static final Pattern CLASSIC_FOR = Pattern.compile(
"\\bfor\\s*\\(\\s*([A-Za-z_][\\w.<>\\[\\] \\t]*?(?:\\[\\s*\\])*)\\s+(\\w+)\\s*=");
private static final Pattern FOR_IN = Pattern.compile(
"\\bfor\\s*\\(\\s*(?:var|let|const)\\s+(\\w+)\\s+in\\s+([^)]+)\\)");
private static final Pattern FOR_OF = Pattern.compile(
"\\bfor\\s*\\(\\s*(?:var|let|const)\\s+(\\w+)\\s+of\\s+([^)]+)\\)");
static final Pattern FOR_IN_OF_LOOKAHEAD = Pattern.compile("^\\s+(?:in|of)\\s");
private static final Set<String> SKIP_TYPES = new HashSet<>(Arrays.asList(
"return", "if", "while", "for", "switch", "catch", "new", "throw"));
private final ScriptDocument doc;
private final String text;
LoopVariableParser(ScriptDocument doc, String text) {
this.doc = doc;
this.text = text;
}
boolean isForInOrOf(String textAfterVarName) {
return FOR_IN_OF_LOOKAHEAD.matcher(textAfterVarName).find();
}
boolean isInsideForHeader(int bodyStart, int absPos) {
int enclosingParen = doc.findEnclosingParenStart(bodyStart, absPos);
return enclosingParen >= 0 && "for".equals(doc.readKeywordBefore(enclosingParen));
}
void parse(Map<Integer, Map<String, List<FieldInfo>>> methodLocals) {
boolean js = doc.isJavaScript();
for (MethodInfo method : doc.getAllMethods()) {
int bodyStart = method.getBodyStart();
int bodyEnd = method.getBodyEnd();
if (bodyStart < 0 || bodyEnd <= bodyStart) continue;
String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length()));
Map<String, List<FieldInfo>> locals = methodLocals.computeIfAbsent(method.getDeclarationOffset(), k -> new HashMap<>());
scanRange(bodyText, bodyStart, bodyEnd, method, locals, js);
}
for (InnerCallableScope scope : doc.getInnerScopes()) {
int bodyStart = scope.getBodyStart();
int bodyEnd = scope.getBodyEnd();
if (bodyStart < 0 || bodyEnd <= bodyStart) continue;
String bodyText = text.substring(bodyStart, Math.min(bodyEnd, text.length()));
MethodInfo ownerMethod = doc.findMethodAtPosition(bodyStart);
if (ownerMethod == null) continue;
Map<String, List<FieldInfo>> locals = methodLocals.computeIfAbsent(ownerMethod.getDeclarationOffset(), k -> new HashMap<>());
scanRange(bodyText, bodyStart, bodyEnd, ownerMethod, locals, js);
}
scanTopLevelLoops();
}
private void scanTopLevelLoops() {
boolean js = doc.isJavaScript();
if (!js) {
for (Matcher m = ENHANCED_FOR.matcher(text); m.find(); ) {
int absForPos = m.start();
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
String typeName = m.group(1).trim();
String varName = m.group(2);
TypeInfo typeInfo = doc.resolveType(typeName);
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
FieldInfo fi = FieldInfo.localField(varName, typeInfo, m.start(2), null, -1, -1, 0);
fi.setScopeInfo(scope);
doc.addTopLevelLocal(fi);
}
for (Matcher m = CLASSIC_FOR.matcher(text); m.find(); ) {
int absForPos = m.start();
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
String typeName = m.group(1).trim();
String varName = m.group(2);
if (SKIP_TYPES.contains(typeName)) continue;
TypeInfo typeInfo = doc.resolveType(typeName);
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
FieldInfo fi = FieldInfo.localField(varName, typeInfo, m.start(2), null, -1, -1, 0);
fi.setScopeInfo(scope);
doc.addTopLevelLocal(fi);
}
} else {
for (Matcher m = FOR_IN.matcher(text); m.find(); ) {
int absForPos = m.start();
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
String varName = m.group(1);
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
FieldInfo fi = FieldInfo.localField(varName, TypeInfo.fromPrimitive("int"), m.start(1), null, -1, -1, 0);
fi.setScopeInfo(scope);
doc.addTopLevelLocal(fi);
}
for (Matcher m = FOR_OF.matcher(text); m.find(); ) {
int absForPos = m.start();
if (doc.isExcluded(absForPos) || isInsideAnyScope(absForPos)) continue;
String varName = m.group(1);
String iterableExpr = m.group(2).trim();
TypeInfo iterableType = doc.resolveExpressionType(iterableExpr, absForPos);
TypeInfo elementType = (iterableType != null) ? iterableType.getElementType() : null;
if (elementType == null) elementType = TypeInfo.ANY;
ScopeInfo scope = computeForBodyScope(absForPos, 0, text.length());
FieldInfo fi = FieldInfo.localField(varName, elementType, m.start(1), null, -1, -1, 0);
fi.setScopeInfo(scope);
doc.addTopLevelLocal(fi);
}
}
}
private boolean isInsideAnyScope(int pos) {
for (MethodInfo method : doc.getAllMethods()) {
if (method.containsPosition(pos)) return true;
}
for (InnerCallableScope scope : doc.getInnerScopes()) {
if (scope.containsPosition(pos)) return true;
}
return false;
}
private void scanRange(String bodyText, int bodyStart, int bodyEnd,
MethodInfo method, Map<String, List<FieldInfo>> locals, boolean js) {
if (!js) {
Matcher m = ENHANCED_FOR.matcher(bodyText);
while (m.find()) {
int absForPos = bodyStart + m.start();
if (doc.isExcluded(absForPos)) continue;
String typeName = m.group(1).trim();
String varName = m.group(2);
int absVarPos = bodyStart + m.start(2);
TypeInfo typeInfo = doc.resolveType(typeName);
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
FieldInfo fi = FieldInfo.localField(varName, typeInfo, absVarPos, method, -1, -1, 0);
fi.setScopeInfo(scope);
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
}
m = CLASSIC_FOR.matcher(bodyText);
while (m.find()) {
int absForPos = bodyStart + m.start();
if (doc.isExcluded(absForPos)) continue;
String typeName = m.group(1).trim();
String varName = m.group(2);
if (SKIP_TYPES.contains(typeName)) continue;
int absVarPos = bodyStart + m.start(2);
TypeInfo typeInfo = doc.resolveType(typeName);
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
FieldInfo fi = FieldInfo.localField(varName, typeInfo, absVarPos, method, -1, -1, 0);
fi.setScopeInfo(scope);
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
}
} else {
Matcher m = FOR_IN.matcher(bodyText);
while (m.find()) {
int absForPos = bodyStart + m.start();
if (doc.isExcluded(absForPos)) continue;
String varName = m.group(1);
int absVarPos = bodyStart + m.start(1);
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
FieldInfo fi = FieldInfo.localField(varName, TypeInfo.fromPrimitive("int"), absVarPos, method, -1, -1, 0);
fi.setScopeInfo(scope);
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
}
m = FOR_OF.matcher(bodyText);
while (m.find()) {
int absForPos = bodyStart + m.start();
if (doc.isExcluded(absForPos)) continue;
String varName = m.group(1);
String iterableExpr = m.group(2).trim();
int absVarPos = bodyStart + m.start(1);
TypeInfo iterableType = doc.resolveExpressionType(iterableExpr, absForPos);
TypeInfo elementType = (iterableType != null) ? iterableType.getElementType() : null;
if (elementType == null) elementType = TypeInfo.ANY;
ScopeInfo scope = computeForBodyScope(absForPos, bodyStart, bodyEnd);
FieldInfo fi = FieldInfo.localField(varName, elementType, absVarPos, method, -1, -1, 0);
fi.setScopeInfo(scope);
locals.computeIfAbsent(varName, k -> new ArrayList<>()).add(fi);
}
}
}
private ScopeInfo computeForBodyScope(int forKeywordPos, int bodyStart, int bodyEnd) {
int max = Math.min(bodyEnd, text.length());
int openParen = -1;
for (int i = forKeywordPos; i < max; i++) {
if (doc.isExcluded(i)) continue;
char c = text.charAt(i);
if (c == '(') { openParen = i; break; }
if (!Character.isWhitespace(c) && c != 'f' && c != 'o' && c != 'r') break;
}
if (openParen < 0) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
int closeParen = doc.findMatchingParen(openParen, max);
if (closeParen < 0) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
int after = doc.skipWhitespaceAndExcluded(closeParen + 1, max);
if (after < 0 || after >= max) return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
if (text.charAt(after) == '{') {
int closeBrace = doc.findMatchingBrace(after);
if (closeBrace > 0) return new ScopeInfo(forKeywordPos, closeBrace + 1, false, "block");
return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
}
int stmtEnd = doc.findStatementEnd(after, max);
if (stmtEnd > after) return new ScopeInfo(forKeywordPos, stmtEnd, false, "block");
return new ScopeInfo(forKeywordPos, bodyEnd, false, "block");
}
}
@@ -0,0 +1,405 @@
package noppes.npcs.client.gui.util.script.interpreter;
// ==================== MULTI-DECLARATOR HELPERS ====================
//
// JavaScript and Java both allow declaring multiple variables in a single
// statement, separated by commas:
//
// JS: var a = 1, b = "x", c = player;
// Java: int x = 1, y, z = 3;
//
// The existing regex patterns only match the FIRST declarator because:
// - The JS pattern `(var|let|const)\s+(\w+)` requires a keyword prefix.
// - The Java FIELD_DECL_PATTERN requires a type name prefix.
//
// Continuation declarators after the first comma have no keyword/type prefix,
// so they must be scanned manually. The two helpers below handle this by
// scanning forward from the end of the first declarator for commas at
// nesting depth 0, then extracting the `name [= initializer]` pattern
// for each continuation declarator.
/**
* Static helpers for parsing comma-separated multi-variable declarations.
*
* <p>Extracted from {@link ScriptDocument} to keep the main document class
* focused on orchestration while this class owns the scanning algorithms.</p>
*/
public final class MultiDeclaratorParser {
private MultiDeclaratorParser() {}
// ======================== CALLBACKS ========================
/** Callback for {@link #scanJSContinuationDeclarators}. */
@FunctionalInterface
public interface JSContinuationCallback {
void accept(String varName, int absNamePos, String initializer, int absInitStart, int absInitEnd);
}
/** Callback for {@link #scanJavaContinuationDeclarators}. */
@FunctionalInterface
public interface JavaContinuationCallback {
void accept(String varName, int absNamePos, int absInitStart, int absInitEnd);
}
// ======================== JS SCANNING ========================
/**
* Scan forward from {@code scanStart} in {@code source} for comma-separated
* continuation declarators in a JS {@code var/let/const} statement.
*
* <p>Each continuation declarator follows the pattern: {@code name [= initializer]}.
* The initializer (if present) is terminated using comma-aware scanning via
* {@link ScriptDocument#findJsInitializerEnd(String, int, boolean)}, which stops at
* top-level commas so that each declarator's RHS is correctly bounded.</p>
*
* <p>The callback receives (varName, absNamePos, initializer, absInitStart, absInitEnd)
* for each continuation declarator found.</p>
*
* @param doc the owning ScriptDocument (provides {@code findJsInitializerEnd}
* and {@code skipSegmentWhitespace})
* @param source the source text to scan within (e.g. method body or global text)
* @param scanStart the position in {@code source} to begin scanning (typically
* right after the first declarator's initializer end or name end)
* @param absBase the absolute offset of {@code source[0]} in the full document text
* (used to compute absolute positions for the callback)
* @param callback receives each continuation declarator
*/
public static void scanJSContinuationDeclarators(
ScriptDocument doc, String source, int scanStart, int absBase,
JSContinuationCallback callback) {
int pos = scanStart;
while (pos < source.length()) {
// Skip whitespace
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
if (pos >= source.length()) break;
// Expect a comma separating the next declarator
if (source.charAt(pos) != ',') break;
pos++; // skip the comma
// Skip whitespace after comma
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
if (pos >= source.length()) break;
// Read the variable name (must be an identifier)
int nameStart = pos;
while (pos < source.length() && Character.isJavaIdentifierPart(source.charAt(pos))) pos++;
if (pos == nameStart) break; // no identifier found; malformed
String varName = source.substring(nameStart, pos);
// Skip whitespace after name
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
String initializer = null;
int initStart = -1;
int initEnd = -1;
// Check for optional '=' initializer
if (pos < source.length() && source.charAt(pos) == '=') {
// Make sure it's not ==
if (pos + 1 < source.length() && source.charAt(pos + 1) == '=') break;
pos++; // skip '='
int rhsStart = doc.skipSegmentWhitespace(source, pos);
// Use comma-aware initializer termination so that the RHS stops at
// the next top-level comma (which starts the next declarator).
int rhsEnd = doc.findJsInitializerEnd(source, rhsStart, true);
if (rhsEnd > rhsStart) {
initializer = source.substring(rhsStart, rhsEnd).trim();
if (initializer.isEmpty()) initializer = null;
}
initStart = rhsStart;
initEnd = rhsEnd;
pos = rhsEnd;
}
int absNamePos = absBase + nameStart;
int absInitStart = initStart >= 0 ? absBase + initStart : -1;
int absInitEnd = initEnd >= 0 ? absBase + initEnd : -1;
callback.accept(varName, absNamePos, initializer, absInitStart, absInitEnd);
}
}
// ======================== JAVA SCANNING ========================
/**
* Scan forward from {@code scanStart} in {@code source} for comma-separated
* continuation declarators in a Java multi-variable declaration.
*
* <p>Java allows {@code int x = 1, y, z = 3;} where all declarators share
* the same declared type. The {@code FIELD_DECL_PATTERN} regex only matches
* the first declarator ({@code int x =}); subsequent names like {@code y}
* and {@code z} lack a type prefix. This method manually scans the remainder
* of the statement for those continuation names.</p>
*
* <p>Each continuation declarator follows the pattern: {@code name [= initializer]}.
* The initializer is terminated at {@code ;} or {@code ,} at depth 0.</p>
*
* @param source the source text to scan within
* @param scanStart the position in {@code source} to begin scanning (typically
* right after the semicolon/delimiter of the first declarator
* match, or right after the first declarator's initializer)
* @param absBase the absolute offset of {@code source[0]} in the full document text
* @param callback receives each continuation declarator
*/
public static void scanJavaContinuationDeclarators(
String source, int scanStart, int absBase,
JavaContinuationCallback callback) {
int pos = scanStart;
while (pos < source.length()) {
// Skip whitespace
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
if (pos >= source.length()) break;
// Expect a comma separating the next declarator
if (source.charAt(pos) != ',') break;
pos++; // skip the comma
// Skip whitespace after comma
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
if (pos >= source.length()) break;
// Read the variable name (must be an identifier)
int nameStart = pos;
while (pos < source.length() && Character.isJavaIdentifierPart(source.charAt(pos))) pos++;
if (pos == nameStart) break; // no identifier found; malformed
String varName = source.substring(nameStart, pos);
// Skip whitespace after name
while (pos < source.length() && Character.isWhitespace(source.charAt(pos))) pos++;
int initStart = -1;
int initEnd = -1;
// Check for optional '=' initializer
if (pos < source.length() && source.charAt(pos) == '=') {
// Make sure it's not ==
if (pos + 1 < source.length() && source.charAt(pos + 1) == '=') break;
initStart = pos; // position of '='
pos++; // skip '='
int rhsStart = pos;
// Skip whitespace after '='
while (rhsStart < source.length() && Character.isWhitespace(source.charAt(rhsStart))) rhsStart++;
// Scan forward for ';' or ',' at depth 0 to terminate this declarator's initializer
int depth = 0;
int angleDepth = 0;
int scanPos = rhsStart;
while (scanPos < source.length()) {
char c = source.charAt(scanPos);
if (c == '(' || c == '[' || c == '{') depth++;
else if (c == ')' || c == ']' || c == '}') depth--;
else if (c == '<') angleDepth++;
else if (c == '>') angleDepth--;
else if ((c == ';' || c == ',') && depth == 0 && angleDepth == 0) {
initEnd = scanPos;
break;
}
scanPos++;
}
pos = (initEnd >= 0) ? initEnd : scanPos;
} else if (pos < source.length() && (source.charAt(pos) == ';' || source.charAt(pos) == ',')) {
// No initializer — name only (e.g., the `y` in `int x, y, z;`)
// Don't advance past ';' here; let the outer loop handle it.
// If it's a comma, we'll pick it up on the next iteration.
} else {
// Unexpected character; stop scanning
break;
}
int absNamePos = absBase + nameStart;
int absInitStartAbs = initStart >= 0 ? absBase + initStart : -1;
int absInitEndAbs = initEnd >= 0 ? absBase + initEnd : -1;
callback.accept(varName, absNamePos, absInitStartAbs, absInitEndAbs);
}
}
// ======================== SCAN-START POSITION HELPERS ========================
/**
* Determine the scan position for continuation declarators after a JS
* declarator match. If the first declarator has an initializer, we scan
* from the initializer end; otherwise we scan from after the variable name.
*
* @param bodyText the body text being parsed
* @param matchEnd the end position of the regex match (end of var name or '=')
* @param hasEquals whether the first declarator has '='
* @param initEnd the initializer end position (in bodyText coords), or -1
* @return the position in bodyText to start scanning for continuation commas
*/
public static int jsDeclaratorScanStart(String bodyText, int matchEnd, boolean hasEquals, int initEnd) {
if (hasEquals && initEnd >= 0) {
return initEnd;
}
// No initializer: scan from after the variable name
return matchEnd;
}
/**
* Determine the scan position for continuation declarators after a Java
* FIELD_DECL_PATTERN match. The pattern's group(3) matches either '=', ';',
* or ','. If it matched '=', the scan starts after the initializer expression;
* if ';', there are no continuations (semicolon ends the statement).
*
* <p><b>Note:</b> when the regex greedily consumed commas into the type name
* (e.g. {@code "int x, y,"} as the type group), the caller must detect this
* via {@link #findFirstDepthZeroComma(String)} on the matched type string
* and use the returned comma position directly as the scan start, bypassing
* this method. See the call sites in {@link ScriptDocument} for details.</p>
*
* @param source the source text
* @param delimiter the delimiter matched by group(3): "=", ";", or ","
* @param delimEnd the end position of the delimiter in source coords
* @param initEnd the initializer end position, or -1 if no initializer
* @return the position in source to start scanning for continuation commas,
* or -1 if the statement ended at a semicolon
*/
public static int javaDeclaratorScanStart(String source, String delimiter, int delimEnd, int initEnd) {
if (";".equals(delimiter)) {
return -1; // semicolon terminates the statement; no continuations possible
}
// delimiter is '=' or ',' — scan from after the initializer (which stops at ',' or ';')
if (initEnd >= 0) {
return initEnd;
}
// When delimiter is ',' with no initializer (e.g. the 'x' in "int x,y,z;"),
// m.end(3) lands AFTER the comma. The scanner expects to START at the comma
// so it can consume it as the continuation separator — so back up by one.
if (",".equals(delimiter)) {
return delimEnd - 1;
}
return delimEnd;
}
// ======================== GREEDY-REGEX COMMA HELPERS ========================
/**
* Find the first comma at bracket-depth 0 in the given string.
*
* <p>The Java {@code FIELD_DECL_PATTERN} regex allows commas inside the
* type-name character class (to support generics like {@code Map<K, V>}).
* This causes the regex to greedily consume multi-declarator commas into
* group(1). For example, {@code "int x, y, z;"} matches with
* group(1)={@code "int x, y,"} and group(2)={@code "z"}.</p>
*
* <p>This helper scans for the first comma that is NOT inside angle brackets
* ({@code <>}), parentheses, square brackets, or braces, i.e. at depth 0.
* A depth-0 comma indicates a multi-declarator separator that was consumed
* by the greedy regex.</p>
*
* @param s the string to search (typically the matched type group)
* @return the index of the first depth-0 comma, or -1 if none found
*/
public static int findFirstDepthZeroComma(String s) {
int depth = 0;
int angleDepth = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(' || c == '[' || c == '{') depth++;
else if (c == ')' || c == ']' || c == '}') depth--;
else if (c == '<') angleDepth++;
else if (c == '>') angleDepth--;
else if (c == ',' && depth == 0 && angleDepth == 0) return i;
}
return -1;
}
/**
* Extract the real type name from a regex group(1) that greedily consumed
* extra variable names and commas.
*
* <p>When the regex matches {@code "int x, y, z;"} as group(1)={@code "int x, y,"},
* the real type name is {@code "int"} — everything before the first identifier
* that precedes the first depth-0 comma.</p>
*
* <p>For {@code "final int x, y,"}, returns {@code "final int"}.
* For {@code "Map<String, Integer> x, y,"}, returns {@code "Map<String, Integer>"}.</p>
*
* @param typeGroup the full group(1) string from the regex match
* @param commaIndex the index of the first depth-0 comma in {@code typeGroup}
* (as returned by {@link #findFirstDepthZeroComma(String)})
* @return the real type name (may include modifiers like "final")
*/
public static String extractRealTypeFromGreedyMatch(String typeGroup, int commaIndex) {
// The part before the first depth-0 comma contains "Type firstVar"
// (possibly with modifiers). We need to split off the last identifier
// token before the comma, as that's the first variable name, not the type.
String beforeComma = typeGroup.substring(0, commaIndex).trim();
int lastSpace = beforeComma.lastIndexOf(' ');
int lastTab = beforeComma.lastIndexOf('\t');
int lastWS = Math.max(lastSpace, lastTab);
if (lastWS < 0) {
// No whitespace found — the "type" is actually just a variable name
// This shouldn't happen for valid declarations but handle gracefully
return beforeComma;
}
return beforeComma.substring(0, lastWS).trim();
}
/**
* Extract the first variable name from a regex group(1) that greedily consumed
* extra variable names and commas.
*
* <p>When the regex matches {@code "int x, y, z;"} as group(1)={@code "int x, y,"},
* the first variable name is {@code "x"} — the last identifier token before
* the first depth-0 comma.</p>
*
* @param typeGroup the full group(1) string from the regex match
* @param commaIndex the index of the first depth-0 comma in {@code typeGroup}
* (as returned by {@link #findFirstDepthZeroComma(String)})
* @return the first variable name
*/
public static String extractFirstVarFromGreedyMatch(String typeGroup, int commaIndex) {
String beforeComma = typeGroup.substring(0, commaIndex).trim();
int lastSpace = beforeComma.lastIndexOf(' ');
int lastTab = beforeComma.lastIndexOf('\t');
int lastWS = Math.max(lastSpace, lastTab);
if (lastWS < 0) {
return beforeComma;
}
return beforeComma.substring(lastWS + 1).trim();
}
/**
* Find the absolute position of the first variable name within a greedily-consumed
* type group. Searches for the first variable name (the last token before the
* first depth-0 comma) within the source text starting from the type group start.
*
* @param source the full source text
* @param typeGroupStart the absolute start of group(1) in the source
* @param typeGroup the group(1) string
* @param commaIndex the index of the first depth-0 comma in typeGroup
* @return the absolute position of the first variable name in source
*/
public static int findFirstVarPosition(String source, int typeGroupStart, String typeGroup, int commaIndex) {
String firstVar = extractFirstVarFromGreedyMatch(typeGroup, commaIndex);
// The first variable appears in typeGroup just before the comma.
// Search backwards from commaIndex to find it.
String beforeComma = typeGroup.substring(0, commaIndex).trim();
int varStartInGroup = beforeComma.lastIndexOf(firstVar);
return typeGroupStart + varStartInGroup;
}
// ======================== MISC HELPERS ========================
/**
* Check whether the statement starting at {@code stmtStart} begins with
* a JS declaration keyword ({@code var}, {@code let}, or {@code const}).
* Used by {@link ScriptDocument}'s {@code parseAssignments()} to decide
* whether the RHS of an {@code =} should stop at a top-level comma
* (multi-declarator context).
*/
public static boolean startsWithJSKeyword(String source, int stmtStart, int equalsPos) {
int i = stmtStart;
while (i < equalsPos && Character.isWhitespace(source.charAt(i))) i++;
String prefix = source.substring(i, Math.min(i + 6, equalsPos)).trim();
return prefix.startsWith("var ") || prefix.startsWith("var\t")
|| prefix.startsWith("let ") || prefix.startsWith("let\t")
|| prefix.startsWith("const ") || prefix.startsWith("const\t");
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,431 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.ClientProxy;
import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenErrorMessage;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.type.ImportData;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
/**
* Represents a single line of source code with its tokens.
* Handles token management, navigation, and rendering.
*
* A ScriptLine is responsible for:
* - Storing tokens that fall within this line's character range
* - Managing token navigation (prev/next within line)
* - Rendering the line with proper syntax highlighting
* - Computing indent guides for this line
*/
public class ScriptLine {
private static final char COLOR_CHAR = '\u00A7';
private final String text; // The actual text of this line
public final int globalStart; // Start offset in the full document
public final int globalEnd; // End offset in the full document (exclusive)
public final int lineIndex; // 0-based line number
private final List<Token> tokens = new ArrayList<>();
private final List<Integer> indentGuides = new ArrayList<>(); // Column positions for indent guides
// Navigation
private ScriptLine prev;
private ScriptLine next;
private ScriptDocument parent;
public ScriptLine(String text, int globalStart, int globalEnd, int lineIndex) {
this.text = text;
this.globalStart = globalStart;
this.globalEnd = globalEnd;
this.lineIndex = lineIndex;
}
// ==================== GETTERS ====================
public String getText() { return text; }
public int getGlobalStart() { return globalStart; }
public int getGlobalEnd() { return globalEnd; }
public int getLineIndex() { return lineIndex; }
public int getLength() { return text.length(); }
public List<Token> getTokens() { return Collections.unmodifiableList(tokens); }
public List<Integer> getIndentGuides() { return Collections.unmodifiableList(indentGuides); }
public ScriptDocument getParent() { return parent; }
void setParent(ScriptDocument parent) { this.parent = parent; }
void setPrev(ScriptLine prev) { this.prev = prev; }
void setNext(ScriptLine next) { this.next = next; }
// ==================== NAVIGATION ====================
public ScriptLine prev() { return prev; }
public ScriptLine next() { return next; }
public Token getFirstToken() {
return tokens.isEmpty() ? null : tokens.get(0);
}
public Token getLastToken() {
return tokens.isEmpty() ? null : tokens.get(tokens.size() - 1);
}
public Token getTokenAt(int globalPosition) {
for (Token t : tokens)
if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd())
return t;
return null;
}
public Token getTokenAt(int globalPosition, Predicate<Token> condition) {
for (Token t : tokens)
if (globalPosition >= t.getGlobalStart() && globalPosition <= t.getGlobalEnd() && condition.test(t))
return t;
return null;
}
// ==================== TOKEN MANAGEMENT ====================
/**
* Clear all tokens from this line.
*/
public void clearTokens() {
tokens.clear();
}
/**
* Add a token to this line and set up navigation links.
*/
public void addToken(Token token) {
if (!tokens.isEmpty()) {
Token last = tokens.get(tokens.size() - 1);
last.setNext(token);
token.setPrev(last);
}
token.setParentLine(this);
tokens.add(token);
}
/**
* Build tokens from a list of marks (highlight regions).
* Fills gaps between marks with DEFAULT tokens.
* Attaches metadata from marks to the created tokens.
*
* @param marks List of highlight marks that overlap this line
* @param fullText The complete document text
* @param document The parent ScriptDocument (for context lookups)
*/
public void buildTokensFromMarks(List<Mark> marks, String fullText, ScriptDocument document) {
clearTokens();
int cursor = globalStart;
for (Mark mark : marks) {
// Skip marks that don't overlap this line
if (mark.end <= globalStart || mark.start >= globalEnd) {
continue;
}
// Clamp mark to line boundaries
int tokenStart = Math.max(mark.start, globalStart);
int tokenEnd = Math.min(mark.end, globalEnd);
// Validate bounds
tokenStart = Math.max(0, Math.min(tokenStart, fullText.length()));
tokenEnd = Math.max(0, Math.min(tokenEnd, fullText.length()));
// Add default token for any gap before this mark
if (cursor < tokenStart) {
int gapEnd = Math.min(tokenStart, fullText.length());
String gapText = fullText.substring(cursor, gapEnd);
addToken(Token.defaultToken(gapText, cursor, gapEnd));
}
// Add the marked token with metadata
if (tokenStart < tokenEnd) {
String tokenText = fullText.substring(tokenStart, tokenEnd);
Token token = new Token(tokenText, tokenStart, tokenEnd, mark.type);
// Attach metadata based on type
if (mark.metadata != null)
applyTokenMetadata(token, mark.metadata);
addToken(token);
}
cursor = tokenEnd;
}
// Add trailing default token if needed
if (cursor < globalEnd) {
int end = Math.min(globalEnd, fullText.length());
if (cursor < end) {
String trailingText = fullText.substring(cursor, end);
addToken(Token.defaultToken(trailingText, cursor, end));
}
}
}
public void applyTokenMetadata(Token token, Object metadata) {
if (metadata instanceof TypeInfo) {
token.setTypeInfo((TypeInfo) metadata);
} else if (metadata instanceof MethodCallInfo) {
MethodCallInfo callInfo = (MethodCallInfo) metadata;
if (callInfo.isConstructor()) {
token.setTypeInfo(callInfo.getReceiverType());
token.setMethodInfo(callInfo.getResolvedMethod());
}
token.setMethodCallInfo(callInfo);
} else if (metadata instanceof FieldInfo.ArgInfo) {
FieldInfo.ArgInfo ctx = (FieldInfo.ArgInfo) metadata;
token.setFieldInfo(ctx.fieldInfo);
token.setMethodCallInfo(ctx.methodCallInfo);
} else if (metadata instanceof FieldInfo) {
token.setFieldInfo((FieldInfo) metadata);
} else if (metadata instanceof MethodInfo) {
token.setMethodInfo((MethodInfo) metadata);
} else if (metadata instanceof ImportData) {
token.setImportData((ImportData) metadata);
} else if (metadata instanceof FieldAccessInfo) {
FieldAccessInfo accessInfo = (FieldAccessInfo) metadata;
token.setFieldAccessInfo(accessInfo);
token.setFieldInfo(accessInfo.getResolvedField());
} else if (metadata instanceof TokenErrorMessage) {
token.setErrorMessage((TokenErrorMessage) metadata);
}
}
// ==================== INDENT GUIDES ====================
/**
* Clear indent guides for this line.
*/
public void clearIndentGuides() {
indentGuides.clear();
}
/**
* Add an indent guide at the specified column.
*/
public void addIndentGuide(int column) {
if (!indentGuides.contains(column)) {
indentGuides.add(column);
}
}
// ==================== RENDERING ====================
/**
* Draw this line with syntax highlighting using Minecraft color codes.
* Compatible with the existing rendering system.
* Also draws curly underlines for tokens with errors (method call validation failures).
* Supports bold (§l) and italic (§o) formatting for certain token types.
*/
public void drawString(int x, int y, int defaultColor) {
StringBuilder builder = new StringBuilder();
int lastIndex = 0;
// Track positions for underlines
int currentX = x;
for (Token t : tokens) {
int tokenStart = t.getGlobalStart() - globalStart; // relative position in line
int tokenWidth = ClientProxy.Font.width(t.getText());
// Calculate gap width before this token
if (tokenStart > lastIndex && tokenStart <= text.length()) {
String gapText = text.substring(lastIndex, tokenStart);
builder.append(gapText);
currentX += ClientProxy.Font.width(gapText);
}
// Append style codes (bold/italic) if applicable
String stylePrefix = t.getStylePrefix();
// Append the colored token with style
builder.append(COLOR_CHAR)
.append(t.getColorCode())
.append(stylePrefix)
.append(t.getText())
.append(COLOR_CHAR)
.append('r'); // Reset all formatting (color + bold/italic)
currentX += tokenWidth;
lastIndex = tokenStart + t.getText().length();
}
// Append any remaining text after the last token
if (lastIndex < text.length()) {
builder.append(text.substring(lastIndex));
}
// Draw the text
ClientProxy.Font.drawString(builder.toString(), x, y, defaultColor);
// Minecraft.getMinecraft().fontRenderer.drawString(builder.toString(), x, y, defaultColor);
// Draw underlines for validation errors (method calls and field accesses)
drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1);
}
/**
* 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.
*/
public void drawStringHex(int x, int y) {
// Build the complete text with all tokens and gaps, draw it as ONE string
// to match the spacing behavior of drawString which draws everything at once
StringBuilder fullText = new StringBuilder();
int lastIndex = 0;
// Build segments with token position info
java.util.List<TextSegment> segments = new java.util.ArrayList<>();
for (Token t : tokens) {
int tokenStart = t.getGlobalStart() - globalStart;
// Add any gap before this token
if (tokenStart > lastIndex && tokenStart <= text.length()) {
String gap = text.substring(lastIndex, tokenStart);
segments.add(new TextSegment(fullText.length(), gap, 0xFFFFFFFF, false));
fullText.append(gap);
}
// Add the colored token
String styledText = t.getStylePrefix() + t.getText();
segments.add(new TextSegment(fullText.length(), styledText, t.getHexColor(), true));
fullText.append(styledText);
lastIndex = tokenStart + styledText.length();
}
// Add any remaining text after the last token
if (lastIndex < text.length()) {
String remaining = text.substring(lastIndex);
segments.add(new TextSegment(fullText.length(), remaining, 0xFFFFFFFF, false));
fullText.append(remaining);
}
// Draw each segment at the correct position
// Calculate positions based on the full string to match drawString's spacing
for (TextSegment seg : segments) {
if (!seg.text.isEmpty()) {
// Get the width of everything before this segment
String prefix = fullText.substring(0, seg.startPos);
int prefixWidth = ClientProxy.Font.width(prefix);
int color = (seg.color & 0xFF000000) == 0 ? (0xFF000000 | seg.color) : seg.color;
// Draw this segment at the correct position
ClientProxy.Font.drawString(seg.text, x + prefixWidth, y, color);
// Minecraft.getMinecraft().fontRenderer.drawString(seg.text, x + prefixWidth, y, color);
}
}
drawErrorUnderlines(x, y + ClientProxy.Font.height() - 1);
}
/**
* Draw underlines for all validation errors (method calls and field accesses) that intersect this line.
* Delegates to ErrorUnderlineRenderer for the actual rendering logic.
*/
private void drawErrorUnderlines(int lineStartX, int baselineY) {
if (parent == null)
return;
ErrorUnderlineRenderer.drawErrorUnderlines(
parent,
lineStartX,
baselineY,
getText(),
getGlobalStart(),
getGlobalEnd()
);
}
// Helper class to track text segments
private static class TextSegment {
final int startPos;
final String text;
final int color;
final boolean isToken;
TextSegment(int startPos, String text, int color, boolean isToken) {
this.startPos = startPos;
this.text = text;
this.color = color;
this.isToken = isToken;
}
}
// ==================== UTILITIES ====================
/**
* Check if a global position falls within this line.
*/
public boolean containsPosition(int globalPosition) {
return globalPosition >= globalStart && globalPosition < globalEnd;
}
/**
* Convert a global position to a column (local offset) within this line.
*/
public int toColumn(int globalPosition) {
return Math.max(0, Math.min(globalPosition - globalStart, text.length()));
}
/**
* Convert a column (local offset) to a global position.
*/
public int toGlobal(int column) {
return globalStart + Math.max(0, Math.min(column, text.length()));
}
@Override
public String toString() {
return "ScriptLine{" + lineIndex + ": '" + text + "' [" + globalStart + "-" + globalEnd + "], " + tokens.size() + " tokens}";
}
// ==================== MARK CLASS (for token building) ====================
/**
* A simple mark representing a highlighted region.
* Used during token building phase.
* Can optionally carry metadata (TypeInfo, FieldInfo, MethodInfo, ImportData).
*/
public static class Mark {
public final int start;
public final int end;
public final TokenType type;
public final Object metadata;
public Mark(int start, int end, TokenType type) {
this.start = start;
this.end = end;
this.type = type;
this.metadata = null;
}
public Mark(int start, int end, TokenType type, Object metadata) {
this.start = start;
this.end = end;
this.type = type;
this.metadata = metadata;
}
@Override
public String toString() {
return "Mark{" + type + " [" + start + "-" + end + "]" + (metadata != null ? " " + metadata.getClass().getSimpleName() : "") + "}";
}
}
}
@@ -0,0 +1,261 @@
package noppes.npcs.client.gui.util.script.interpreter;
import noppes.npcs.client.ClientProxy;
import noppes.npcs.client.gui.util.script.JavaTextContainer;
import noppes.npcs.constants.ScriptContext;
import java.util.Collections;
import java.util.Map;
/**
* New interpreter-based text container that replaces JavaTextContainer.
*
* This class provides the same interface as JavaTextContainer but uses the
* new modular interpreter system internally. It produces compatible LineData
* objects for seamless integration with GuiScriptTextArea.
*
* Key differences from JavaTextContainer:
* - Clean single-pass tokenization with 7 distinct phases
* - Proper token navigation via prev()/next() methods
* - Type-specific metadata on tokens (TypeInfo, FieldInfo, MethodInfo)
* - Better separation of concerns (TypeResolver, ScriptDocument, ScriptLine)
* - Hex color support in addition to Minecraft color codes
*/
public class ScriptTextContainer extends JavaTextContainer {
/**
* Feature flag to enable/disable the new interpreter system.
* When false, falls back to original JavaTextContainer behavior.
*/
public static boolean USE_NEW_INTERPRETER = true;
private ScriptDocument document;
/** The scripting language: "ECMAScript", "Groovy", etc. */
private String language = "ECMAScript";
/** The script context: NPC, PLAYER, BLOCK, ITEM, etc. */
private ScriptContext scriptContext = ScriptContext.GLOBAL;
/** Editor-injected globals (name -> type name) */
private Map<String, String> editorGlobals = Collections.emptyMap();
public ScriptTextContainer(String text) {
super(text);
if (USE_NEW_INTERPRETER) {
document = new ScriptDocument(text);
}
}
public ScriptTextContainer(String text, String language) {
super(text);
this.language = language != null ? language : "ECMAScript";
if (USE_NEW_INTERPRETER) {
document = new ScriptDocument(text, this.language);
}
}
/**
* Set the scripting language. This affects syntax highlighting and type inference.
* @param language The language name (e.g., "ECMAScript", "Groovy")
*/
public void setLanguage(String language) {
this.language = language != null ? language : "ECMAScript";
if (document != null) {
document.setLanguage(this.language);
}
}
/**
* Get the current scripting language.
*/
public String getLanguage() {
return language;
}
/**
* Set the script context (NPC, PLAYER, BLOCK, ITEM, etc.).
* This determines which hooks and event types are available for autocomplete.
*
* @param context The script context
*/
public void setScriptContext(ScriptContext context) {
this.scriptContext = context != null ? context : ScriptContext.GLOBAL;
if (document != null) {
document.setScriptContext(this.scriptContext);
}
}
/**
* Get the current script context.
*
* @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.)
*/
public ScriptContext getScriptContext() {
return scriptContext;
}
/**
* Set editor-injected globals (name -> type name).
*/
public void setEditorGlobalsMap(java.util.Map<String, String> globals) {
this.editorGlobals = globals != null ? globals : java.util.Collections.emptyMap();
if (document != null) {
document.setEditorGlobals(this.editorGlobals);
}
}
/**
* Add implicit imports that should be resolved without explicit import statements.
* Used for JaninoScript default imports and hook parameter types.
*
* @param patterns Array of import patterns to add (wildcard packages like "noppes.npcs.api.*" or FQ class names)
*/
public void addImplicitImports(String... patterns) {
if (document != null) {
document.addImplicitImports(patterns);
}
}
@Override
public void init(int width, int height) {
if (!USE_NEW_INTERPRETER) {
super.init(width, height);
return;
}
lineHeight = ClientProxy.Font.height();
if (lineHeight == 0) lineHeight = 12;
// Initialize the document
document.setText(text);
document.init(width, height);
// Convert ScriptLines to LineData for compatibility
rebuildLineData();
linesCount = lines.size();
totalHeight = linesCount * lineHeight;
visibleLines = Math.max(height / lineHeight - 1, 1);
}
/**
* Initialize with explicit text (called when text changes).
*/
@Override
public void init(String text, int width, int height) {
this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n");
if (!USE_NEW_INTERPRETER) {
super.init(text, width, height);
return;
}
if (document == null) {
document = new ScriptDocument(this.text, this.language);
document.setScriptContext(this.scriptContext);
document.setEditorGlobals(this.editorGlobals);
}
init(width, height);
}
/**
* Convert ScriptDocument lines to the legacy LineData format.
*/
private void rebuildLineData() {
lines.clear();
for (ScriptLine scriptLine : document.getLines()) {
LineData ld = new LineData(
scriptLine.getText(),
scriptLine.getGlobalStart(),
scriptLine.getGlobalEnd()
);
// Copy indent guides
ld.indentCols.addAll(scriptLine.getIndentGuides());
// Convert tokens - use fully qualified name to avoid conflict with inherited Token
for (noppes.npcs.client.gui.util.script.interpreter.token.Token interpreterToken : scriptLine.getTokens()) {
ld.tokens.add(new JavaTextContainer.Token(
interpreterToken.getText(),
toLegacyTokenType(interpreterToken.getType()),
interpreterToken.getGlobalStart(),
interpreterToken.getGlobalEnd()
));
}
lines.add(ld);
}
}
/**
* Main formatting entry point - matches JavaTextContainer signature.
*/
@Override
public void formatCodeText() {
if (!USE_NEW_INTERPRETER) {
super.formatCodeText();
return;
}
document.formatCodeText();
rebuildLineData();
}
/**
* Convert new interpreter TokenType to legacy JavaTextContainer.TokenType format.
*/
private JavaTextContainer.TokenType toLegacyTokenType(
noppes.npcs.client.gui.util.script.interpreter.token.TokenType type) {
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.COMMENT) return JavaTextContainer.TokenType.COMMENT;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.STRING) return JavaTextContainer.TokenType.STRING;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.CLASS_KEYWORD) return JavaTextContainer.TokenType.CLASS_KEYWORD;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.IMPORT_KEYWORD) return JavaTextContainer.TokenType.IMPORT_KEYWORD;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.KEYWORD) return JavaTextContainer.TokenType.KEYWORD;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.MODIFIER) return JavaTextContainer.TokenType.MODIFIER;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.INTERFACE_DECL) return JavaTextContainer.TokenType.INTERFACE_DECL;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.ENUM_DECL) return JavaTextContainer.TokenType.ENUM_DECL;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.CLASS_DECL) return JavaTextContainer.TokenType.CLASS_DECL;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.IMPORTED_CLASS) return JavaTextContainer.TokenType.IMPORTED_CLASS;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.TYPE_DECL) return JavaTextContainer.TokenType.TYPE_DECL;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.METHOD_DECL) return JavaTextContainer.TokenType.METHOD_DECARE;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.METHOD_CALL) return JavaTextContainer.TokenType.METHOD_CALL;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.LITERAL) return JavaTextContainer.TokenType.NUMBER;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.GLOBAL_FIELD) return JavaTextContainer.TokenType.GLOBAL_FIELD;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.LOCAL_FIELD) return JavaTextContainer.TokenType.LOCAL_FIELD;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.PARAMETER) return JavaTextContainer.TokenType.PARAMETER;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.UNDEFINED_VAR) return JavaTextContainer.TokenType.UNDEFINED_VAR;
if (type == noppes.npcs.client.gui.util.script.interpreter.token.TokenType.VARIABLE) return JavaTextContainer.TokenType.VARIABLE;
return JavaTextContainer.TokenType.DEFAULT;
}
// ==================== ACCESSORS ====================
/**
* Get the underlying ScriptDocument for advanced operations.
*/
public ScriptDocument getDocument() {
return document;
}
/**
* Get the interpreter Token at a specific global position in the text.
* Returns null if no token is at that position or if the interpreter is disabled.
*
* @param globalPosition Position in the document text
* @return The Token at that position, or null
*/
public noppes.npcs.client.gui.util.script.interpreter.token.Token getInterpreterTokenAt(int globalPosition) {
if (!USE_NEW_INTERPRETER || document == null) {
return null;
}
ScriptLine line = document.getLineAt(globalPosition);
if (line == null) {
return null;
}
return line.getTokenAt(globalPosition);
}
}
@@ -0,0 +1,307 @@
package noppes.npcs.client.gui.util.script.interpreter.bridge;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public final class DtsJavaBridge {
/** Cache of reflection Method -> matching JS method info (for docs/param names). */
private static final Map<Method, JSMethodInfo> METHOD_CACHE = new ConcurrentHashMap<>();
/** Cache of reflection Field -> matching JS field info (for docs/typing). */
private static final Map<Field, JSFieldInfo> FIELD_CACHE = new ConcurrentHashMap<>();
/** Cache of reflection Method -> resolved Java TypeInfo override from .d.ts. */
private static final Map<Method, TypeInfo> RETURN_OVERRIDE_CACHE = new ConcurrentHashMap<>();
/** Negative cache for methods with no usable override. */
private static final Set<Method> RETURN_OVERRIDE_MISSES = ConcurrentHashMap.newKeySet();
private DtsJavaBridge() {}
public static void clearCache() {
METHOD_CACHE.clear();
FIELD_CACHE.clear();
RETURN_OVERRIDE_CACHE.clear();
RETURN_OVERRIDE_MISSES.clear();
}
/**
* Resolve a Java return type override for a reflected method using its .d.ts twin.
*
* The override is used only for editor typing (e.g., Janino hover/autocomplete) and
* never changes runtime reflection. This returns a Java-resolved TypeInfo when the
* .d.ts return type can be mapped to a concrete Java class.
*/
public static TypeInfo resolveReturnTypeOverride(Method method, TypeInfo containingType, JSMethodInfo jsMethod) {
if (method == null || jsMethod == null) return null;
TypeInfo cached = RETURN_OVERRIDE_CACHE.get(method);
if (cached != null) return cached;
if (RETURN_OVERRIDE_MISSES.contains(method)) return null;
String jsReturnType = jsMethod.getReturnType();
if (jsReturnType == null || jsReturnType.isEmpty()) {
RETURN_OVERRIDE_MISSES.add(method);
return null;
}
String normalizedJsReturn = jsReturnType;
if (normalizedJsReturn.contains("|")) {
normalizedJsReturn = normalizedJsReturn.split("\\|")[0].trim();
}
normalizedJsReturn = normalizedJsReturn.replace("?", "").trim();
// Fast-path: skip resolver if the .d.ts return matches reflection (common case).
Class<?> reflectedReturn = method.getReturnType();
if (reflectedReturn != null) {
if (normalizedJsReturn.equals(reflectedReturn.getName())
|| normalizedJsReturn.equals(reflectedReturn.getSimpleName())
|| normalizedJsReturn.equals("Java." + reflectedReturn.getName())) {
RETURN_OVERRIDE_MISSES.add(method);
return null;
}
String lower = normalizedJsReturn.toLowerCase();
if (("void".equals(lower) && (reflectedReturn == void.class || reflectedReturn == Void.class))
|| ("boolean".equals(lower) && (reflectedReturn == boolean.class || reflectedReturn == Boolean.class))
|| ("string".equals(lower) && reflectedReturn == String.class)
|| ("number".equals(lower) && (reflectedReturn == double.class || reflectedReturn == Double.class))) {
RETURN_OVERRIDE_MISSES.add(method);
return null;
}
}
TypeResolver resolver = TypeResolver.getInstance();
TypeInfo resolved = resolver.resolveJSType(normalizedJsReturn);
TypeInfo javaResolved = null;
if (resolved != null) {
if (resolved.getJavaClass() != null) {
javaResolved = resolved;
} else if (resolved.isJSType() && resolved.getJSTypeInfo() != null) {
String javaFqn = resolved.getJSTypeInfo().getJavaFqn();
if (javaFqn != null && !javaFqn.isEmpty()) {
javaResolved = resolver.resolveFullName(javaFqn);
}
}
}
if (javaResolved == null || javaResolved.getJavaClass() == null) {
RETURN_OVERRIDE_MISSES.add(method);
return null;
}
RETURN_OVERRIDE_CACHE.put(method, javaResolved);
return javaResolved;
}
public static JSMethodInfo findMatchingMethod(Method method, TypeInfo containingType) {
JSMethodInfo cached = METHOD_CACHE.get(method);
if (cached != null) return cached;
JSTypeInfo jsType = findJSTypeInfo(method, containingType);
if (jsType == null) return null;
List<JSMethodInfo> overloads = jsType.getMethodOverloads(method.getName());
if (overloads.isEmpty()) return null;
Class<?>[] paramTypes = method.getParameterTypes();
JSMethodInfo best = null;
int bestScore = -1;
for (JSMethodInfo candidate : overloads) {
if (candidate.getParameterCount() != paramTypes.length) continue;
int score = scoreOverload(candidate, paramTypes, containingType, method.isVarArgs());
if (score > bestScore) {
bestScore = score;
best = candidate;
}
}
if (bestScore >= 0 && best != null) {
METHOD_CACHE.put(method, best);
return best;
}
return null;
}
public static JSFieldInfo findMatchingField(Field field, TypeInfo containingType) {
JSFieldInfo cached = FIELD_CACHE.get(field);
if (cached != null) return cached;
JSTypeInfo jsType = findJSTypeInfo(field, containingType);
if (jsType == null) return null;
JSFieldInfo match = jsType.getField(field.getName());
if (match != null) {
FIELD_CACHE.put(field, match);
}
return match;
}
private static int scoreOverload(JSMethodInfo method, Class<?>[] paramTypes, TypeInfo containingType, boolean isVarArgs) {
int score = 0;
List<JSMethodInfo.JSParameterInfo> jsParams = method.getParameters();
for (int i = 0; i < paramTypes.length; i++) {
int paramScore = scoreParam(paramTypes[i], jsParams.get(i), containingType, isVarArgs, i == paramTypes.length - 1);
if (paramScore < 0) return -1;
score += paramScore;
}
return score;
}
private static int scoreParam(Class<?> javaParam, JSMethodInfo.JSParameterInfo jsParam, TypeInfo containingType, boolean isVarArgs, boolean isLastParam) {
if (javaParam == null || jsParam == null) return -1;
TypeInfo resolved = jsParam.getResolvedType(containingType);
if (resolved != null && resolved.getJavaClass() != null) {
Class<?> jsClass = resolved.getJavaClass();
if (javaParam.equals(jsClass)) return 4;
if (jsClass.isAssignableFrom(javaParam)) return 3;
if (javaParam.isAssignableFrom(jsClass)) return 2;
}
String jsTypeName = jsParam.getType();
if (jsTypeName == null || jsTypeName.isEmpty()) return -1;
if (isVarArgs && isLastParam && javaParam.isArray()) {
if (matchesArrayElement(javaParam.getComponentType(), jsTypeName, containingType)) {
return 2;
}
}
if (matchesPrimitive(jsTypeName, javaParam)) {
return 3;
}
if (javaParam.isArray() && jsTypeName.endsWith("[]")) {
String elementType = jsTypeName.substring(0, jsTypeName.length() - 2);
return matchesArrayElement(javaParam.getComponentType(), elementType, containingType) ? 2 : -1;
}
JSTypeInfo jsType = resolveJSTypeInfo(jsTypeName);
if (jsType != null && jsType.getJavaFqn() != null) {
String javaFqn = normalizeJavaFqn(jsType.getJavaFqn());
String paramFqn = normalizeJavaFqn(javaParam.getName());
if (javaFqn.equals(paramFqn)) return 3;
}
if (jsTypeName.equals(javaParam.getSimpleName())) return 1;
if (jsTypeName.equals(javaParam.getName())) return 1;
if ("any".equals(jsTypeName)) return 1;
return -1;
}
private static boolean matchesArrayElement(Class<?> elementClass, String jsElementType, TypeInfo containingType) {
if (elementClass == null) return false;
if (matchesPrimitive(jsElementType, elementClass)) return true;
JSTypeInfo jsType = resolveJSTypeInfo(jsElementType);
if (jsType != null && jsType.getJavaFqn() != null) {
String javaFqn = normalizeJavaFqn(jsType.getJavaFqn());
String elementFqn = normalizeJavaFqn(elementClass.getName());
return javaFqn.equals(elementFqn);
}
return jsElementType.equals(elementClass.getSimpleName()) || jsElementType.equals(elementClass.getName());
}
private static boolean matchesPrimitive(String jsTypeName, Class<?> javaParam) {
switch (jsTypeName.toLowerCase()) {
case "string":
case "char":
return javaParam == String.class || javaParam == char.class || javaParam == Character.class;
case "boolean":
case "bool":
return javaParam == boolean.class || javaParam == Boolean.class;
case "number":
case "byte":
case "short":
case "int":
case "integer":
case "long":
case "float":
case "double":
return isNumberType(javaParam);
case "void":
return javaParam == void.class || javaParam == Void.class;
default:
return false;
}
}
private static boolean isNumberType(Class<?> type) {
if (type == null) return false;
return type == byte.class || type == short.class || type == int.class || type == long.class
|| type == float.class || type == double.class
|| Number.class.isAssignableFrom(type);
}
private static JSTypeInfo findJSTypeInfo(Method method, TypeInfo containingType) {
if (containingType != null && containingType.isJSType()) {
return containingType.getJSTypeInfo();
}
Class<?> javaClass = containingType != null ? containingType.getJavaClass() : method.getDeclaringClass();
return resolveJSTypeInfo(javaClass);
}
private static JSTypeInfo findJSTypeInfo(Field field, TypeInfo containingType) {
if (containingType != null && containingType.isJSType()) {
return containingType.getJSTypeInfo();
}
Class<?> javaClass = containingType != null ? containingType.getJavaClass() : field.getDeclaringClass();
return resolveJSTypeInfo(javaClass);
}
private static JSTypeInfo resolveJSTypeInfo(Class<?> javaClass) {
if (javaClass == null) return null;
JSTypeInfo direct = resolveJSTypeInfo(javaClass.getName());
if (direct != null) return direct;
for (Class<?> iface : javaClass.getInterfaces()) {
JSTypeInfo ifaceType = resolveJSTypeInfo(iface.getName());
if (ifaceType != null) return ifaceType;
}
Class<?> superClass = javaClass.getSuperclass();
if (superClass != null && superClass != Object.class) {
return resolveJSTypeInfo(superClass);
}
return null;
}
private static JSTypeInfo resolveJSTypeInfo(String javaFqnOrType) {
if (javaFqnOrType == null || javaFqnOrType.isEmpty()) return null;
if (javaFqnOrType.startsWith("Java.")) {
javaFqnOrType = javaFqnOrType.substring(5);
}
JSTypeRegistry registry = TypeResolver.getInstance().getJSTypeRegistry();
JSTypeInfo direct = registry.getTypeByJavaFqn(javaFqnOrType);
if (direct != null) return direct;
String normalized = normalizeJavaFqn(javaFqnOrType);
if (!normalized.equals(javaFqnOrType)) {
JSTypeInfo normalizedType = registry.getTypeByJavaFqn(normalized);
if (normalizedType != null) return normalizedType;
}
return registry.getType(javaFqnOrType);
}
private static String normalizeJavaFqn(String javaFqn) {
if (javaFqn == null) return null;
return javaFqn.replace('$', '.');
}
}
@@ -0,0 +1,223 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.List;
import java.util.function.Function;
/**
* Helper class for resolving cast expressions and distinguishing them from parenthesized expressions.
* Handles:
* - Simple casts: (Type) expr
* - Nested casts: ((Type) expr)
* - Cast chains: ((Type) expr).method()
* - Parenthesized expressions: (expr)
*
* @Deprecated ExpressionTypeResolver API has all this built in.
*/
@Deprecated
public class CastExpressionResolver {
/**
* Resolve a cast or parenthesized expression.
*
* @param expr The expression starting with '('
* @param position The position in the source text
* @param resolveType Function to resolve type names to TypeInfo
* @param resolveExpression Function to resolve arbitrary expressions
* @param parseChain Function to parse expression chain
* @param resolveChainSegment Function to resolve chain segments
* @return The resolved type
*/
public static <T> TypeInfo resolveCastOrParenthesizedExpression(
String expr,
int position,
Function<String, TypeInfo> resolveType,
TypeResolver resolveExpression,
ChainParser<T> parseChain,
ChainSegmentResolver<T> resolveChainSegment) {
if (!expr.startsWith("(")) return null;
// Find the matching closing parenthesis for the first open paren
int depth = 0;
int closeParen = -1;
boolean inString = false;
char stringChar = 0;
for (int i = 0; i < expr.length(); i++) {
char c = expr.charAt(i);
// Handle string literals
if ((c == '"' || c == '\'') && (i == 0 || expr.charAt(i - 1) != '\\')) {
if (!inString) {
inString = true;
stringChar = c;
} else if (c == stringChar) {
inString = false;
}
continue;
}
if (inString) continue;
if (c == '(') {
depth++;
} else if (c == ')') {
depth--;
if (depth == 0) {
closeParen = i;
break;
}
}
}
if (closeParen < 0) return null;
// Extract content inside parens
String insideParens = expr.substring(1, closeParen).trim();
String afterParens = expr.substring(closeParen + 1).trim();
// Check if this is a cast expression
// A cast has a type name inside the parens (starts with uppercase or is primitive)
// and is followed by an expression (not an operator or nothing)
if (looksLikeCastType(insideParens) && looksLikeExpressionStart(afterParens)) {
// This is a cast: (Type) rest
TypeInfo castType = resolveType.apply(insideParens);
if (castType != null && castType.isResolved()) {
// If there's more after the cast target, resolve the chain
// E.g., ((EntityPlayer) entity).worldObj
if (!afterParens.isEmpty()) {
// Check if afterParens starts with '.' for a method/field chain
if (afterParens.startsWith(".")) {
// Continue resolving the chain from the cast type
String chainExpr = afterParens.substring(1).trim();
List<T> segments = parseChain.parse(chainExpr);
TypeInfo currentType = castType;
for (T segment : segments) {
currentType = resolveChainSegment.resolve(currentType, segment);
if (currentType == null) return null;
}
return currentType;
}
}
return castType;
}
}
// Not a cast - treat as parenthesized expression
// Could be ((expr).method()) or just (expr)
// First, resolve what's inside the parens
TypeInfo innerType = resolveExpression.resolve(insideParens, position);
// If there's a chain after the parens, continue resolving
if (innerType != null && !afterParens.isEmpty() && afterParens.startsWith(".")) {
String chainExpr = afterParens.substring(1).trim();
List<T> segments = parseChain.parse(chainExpr);
TypeInfo currentType = innerType;
for (T segment : segments) {
currentType = resolveChainSegment.resolve(currentType, segment);
if (currentType == null) return null;
}
return currentType;
}
return innerType;
}
/**
* Check if a string looks like a valid type name for a cast.
* Valid: EntityPlayer, int, String, java.lang.String, int[]
*/
public static boolean looksLikeCastType(String s) {
if (s == null || s.isEmpty()) return false;
s = s.trim();
// Handle array types: Type[] or Type[][]
while (s.endsWith("[]")) {
s = s.substring(0, s.length() - 2).trim();
}
if (s.isEmpty()) return false;
// Primitive types
switch (s) {
case "byte": case "short": case "int": case "long":
case "float": case "double": case "char": case "boolean":
return true;
}
// Class type: starts with uppercase or is fully qualified
if (Character.isUpperCase(s.charAt(0))) return true;
// Fully qualified name: contains dots and ends with uppercase segment
if (s.contains(".")) {
String[] parts = s.split("\\.");
String lastPart = parts[parts.length - 1];
return !lastPart.isEmpty() && Character.isUpperCase(lastPart.charAt(0));
}
return false;
}
/**
* Check if a string looks like the start of an expression (what follows a cast).
* Valid: identifier, (, new, literals, this, etc.
* Invalid: empty, operators like +, -, etc.
*/
public static boolean looksLikeExpressionStart(String s) {
if (s == null || s.isEmpty()) return false;
s = s.trim();
if (s.isEmpty()) return false;
char first = s.charAt(0);
// Parenthesis (nested cast or parenthesized expr)
if (first == '(') return true;
// Identifier or keyword
if (Character.isJavaIdentifierStart(first)) return true;
// String or char literal
if (first == '"' || first == '\'') return true;
// Number literal (could start with digit or dot for .5)
if (Character.isDigit(first)) return true;
if (first == '.' && s.length() > 1 && Character.isDigit(s.charAt(1))) return true;
// Unary operators followed by expression
if (first == '!' || first == '~') return true;
if ((first == '+' || first == '-') && s.length() > 1) {
// Could be unary +/- or ++/--
char second = s.charAt(1);
if (second == first || Character.isDigit(second) || Character.isJavaIdentifierStart(second) || second == '(') {
return true;
}
}
return false;
}
/**
* Functional interface for resolving type expressions.
*/
@FunctionalInterface
public interface TypeResolver {
TypeInfo resolve(String expr, int position);
}
/**
* Functional interface for parsing expression chains.
*/
@FunctionalInterface
public interface ChainParser<T> {
List<T> parse(String expr);
}
/**
* Functional interface for resolving chain segments.
*/
@FunctionalInterface
public interface ChainSegmentResolver<T> {
TypeInfo resolve(TypeInfo currentType, T segment);
}
}
@@ -0,0 +1,403 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public abstract class ExpressionNode {
protected final int start;
protected final int end;
protected ExpressionNode(int start, int end) {
this.start = start;
this.end = end;
}
public int getStart() { return start; }
public int getEnd() { return end; }
public abstract TypeInfo resolveType(TypeResolverContext resolver);
public static class IntLiteralNode extends ExpressionNode {
private final String value;
public IntLiteralNode(String value, int start, int end) { super(start, end); this.value = value; }
public String getValue() { return value; }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("int"); }
}
public static class LongLiteralNode extends ExpressionNode {
public LongLiteralNode(String value, int start, int end) { super(start, end); }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("long"); }
}
public static class FloatLiteralNode extends ExpressionNode {
public FloatLiteralNode(String value, int start, int end) { super(start, end); }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("float"); }
}
public static class DoubleLiteralNode extends ExpressionNode {
public DoubleLiteralNode(String value, int start, int end) { super(start, end); }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("double"); }
}
public static class BooleanLiteralNode extends ExpressionNode {
public BooleanLiteralNode(boolean value, int start, int end) { super(start, end); }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); }
}
public static class CharLiteralNode extends ExpressionNode {
public CharLiteralNode(String value, int start, int end) { super(start, end); }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("char"); }
}
public static class StringLiteralNode extends ExpressionNode {
private final String value;
public StringLiteralNode(String value, int start, int end) { super(start, end); this.value = value; }
public String getValue() { return value; }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.string(); }
}
public static class NullLiteralNode extends ExpressionNode {
public NullLiteralNode(int start, int end) { super(start, end); }
public TypeInfo resolveType(TypeResolverContext resolver) {
return TypeInfo.unresolved("null", "<null>");
}
}
public static class IdentifierNode extends ExpressionNode {
private final String name;
public IdentifierNode(String name, int start, int end) { super(start, end); this.name = name; }
public String getName() { return name; }
public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveIdentifier(name); }
}
public static class MemberAccessNode extends ExpressionNode {
private final ExpressionNode target;
private final String memberName;
public MemberAccessNode(ExpressionNode target, String memberName, int start, int end) {
super(start, end); this.target = target; this.memberName = memberName;
}
public ExpressionNode getTarget() { return target; }
public String getMemberName() { return memberName; }
public TypeInfo resolveType(TypeResolverContext resolver) {
TypeInfo targetType = target.resolveType(resolver);
if (targetType == null || !targetType.isResolved()) return null;
return resolver.resolveMemberAccess(targetType, memberName);
}
}
public static class MethodCallNode extends ExpressionNode {
private final ExpressionNode target;
private final String methodName;
private final List<ExpressionNode> arguments;
public MethodCallNode(ExpressionNode target, String methodName, List<ExpressionNode> arguments, int start, int end) {
super(start, end); this.target = target; this.methodName = methodName;
this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>();
}
public ExpressionNode getTarget() { return target; }
public String getMethodName() { return methodName; }
public List<ExpressionNode> getArguments() { return Collections.unmodifiableList(arguments); }
public TypeInfo resolveType(TypeResolverContext resolver) {
TypeInfo targetType = target != null ? target.resolveType(resolver) : null;
TypeInfo[] argTypes = new TypeInfo[arguments.size()];
for (int i = 0; i < arguments.size(); i++) argTypes[i] = arguments.get(i).resolveType(resolver);
return resolver.resolveMethodCall(targetType, methodName, argTypes);
}
}
public static class ArrayAccessNode extends ExpressionNode {
private final ExpressionNode array;
private final ExpressionNode index;
public ArrayAccessNode(ExpressionNode array, ExpressionNode index, int start, int end) {
super(start, end); this.array = array; this.index = index;
}
public ExpressionNode getArray() { return array; }
public ExpressionNode getIndex() { return index; }
public TypeInfo resolveType(TypeResolverContext resolver) {
TypeInfo arrayType = array.resolveType(resolver);
if (arrayType == null || !arrayType.isResolved()) return null;
return resolver.resolveArrayAccess(arrayType);
}
}
public static class NewNode extends ExpressionNode {
private final String typeName;
private final List<ExpressionNode> arguments;
public NewNode(String typeName, List<ExpressionNode> arguments, int start, int end) {
super(start, end); this.typeName = typeName;
this.arguments = arguments != null ? new ArrayList<>(arguments) : new ArrayList<>();
}
public String getTypeName() { return typeName; }
public List<ExpressionNode> getArguments() { return Collections.unmodifiableList(arguments); }
public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); }
}
public static class BinaryOpNode extends ExpressionNode {
private final ExpressionNode left;
private final OperatorType operator;
private final ExpressionNode right;
public BinaryOpNode(ExpressionNode left, OperatorType operator, ExpressionNode right, int start, int end) {
super(start, end); this.left = left; this.operator = operator; this.right = right;
}
public ExpressionNode getLeft() { return left; }
public OperatorType getOperator() { return operator; }
public ExpressionNode getRight() { return right; }
public TypeInfo resolveType(TypeResolverContext resolver) {
TypeInfo leftType = left.resolveType(resolver);
TypeInfo rightType = right.resolveType(resolver);
return TypeRules.resolveBinaryOperatorType(operator, leftType, rightType);
}
}
public static class UnaryOpNode extends ExpressionNode {
private final OperatorType operator;
private final ExpressionNode operand;
private final boolean prefix;
public UnaryOpNode(OperatorType operator, ExpressionNode operand, boolean prefix, int start, int end) {
super(start, end); this.operator = operator; this.operand = operand; this.prefix = prefix;
}
public OperatorType getOperator() { return operator; }
public ExpressionNode getOperand() { return operand; }
public boolean isPrefix() { return prefix; }
public TypeInfo resolveType(TypeResolverContext resolver) {
TypeInfo operandType = operand.resolveType(resolver);
return TypeRules.resolveUnaryOperatorType(operator, operandType);
}
}
public static class TernaryNode extends ExpressionNode {
private final ExpressionNode condition;
private final ExpressionNode thenExpr;
private final ExpressionNode elseExpr;
public TernaryNode(ExpressionNode condition, ExpressionNode thenExpr, ExpressionNode elseExpr, int start, int end) {
super(start, end); this.condition = condition; this.thenExpr = thenExpr; this.elseExpr = elseExpr;
}
public ExpressionNode getCondition() { return condition; }
public ExpressionNode getThenExpr() { return thenExpr; }
public ExpressionNode getElseExpr() { return elseExpr; }
public TypeInfo resolveType(TypeResolverContext resolver) {
TypeInfo thenType = thenExpr.resolveType(resolver);
TypeInfo elseType = elseExpr.resolveType(resolver);
return TypeRules.resolveTernaryType(thenType, elseType);
}
}
public static class InstanceofNode extends ExpressionNode {
private final ExpressionNode expression;
private final String typeName;
public InstanceofNode(ExpressionNode expression, String typeName, int start, int end) {
super(start, end); this.expression = expression; this.typeName = typeName;
}
public ExpressionNode getExpression() { return expression; }
public String getTypeName() { return typeName; }
public TypeInfo resolveType(TypeResolverContext resolver) { return TypeInfo.fromPrimitive("boolean"); }
}
public static class CastNode extends ExpressionNode {
private final String typeName;
private final ExpressionNode expression;
public CastNode(String typeName, ExpressionNode expression, int start, int end) {
super(start, end); this.typeName = typeName; this.expression = expression;
}
public String getTypeName() { return typeName; }
public ExpressionNode getExpression() { return expression; }
public TypeInfo resolveType(TypeResolverContext resolver) { return resolver.resolveTypeName(typeName); }
}
public static class AssignmentNode extends ExpressionNode {
private final ExpressionNode target;
private final OperatorType operator;
private final ExpressionNode value;
public AssignmentNode(ExpressionNode target, OperatorType operator, ExpressionNode value, int start, int end) {
super(start, end); this.target = target; this.operator = operator; this.value = value;
}
public ExpressionNode getTarget() { return target; }
public OperatorType getOperator() { return operator; }
public ExpressionNode getValue() { return value; }
public TypeInfo resolveType(TypeResolverContext resolver) { return target.resolveType(resolver); }
}
public static class ParenthesizedNode extends ExpressionNode {
private final ExpressionNode inner;
public ParenthesizedNode(ExpressionNode inner, int start, int end) { super(start, end); this.inner = inner; }
public ExpressionNode getInner() { return inner; }
public TypeInfo resolveType(TypeResolverContext resolver) { return inner.resolveType(resolver); }
}
public static class LambdaNode extends ExpressionNode {
private final List<String> parameterNames;
private final ExpressionNode body;
private final boolean isBlock;
private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef;
public LambdaNode(List<String> parameterNames, ExpressionNode body, boolean isBlock, int start, int end) {
super(start, end);
this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>();
this.body = body;
this.isBlock = isBlock;
this.scopeRef = null;
}
public List<String> getParameterNames() { return Collections.unmodifiableList(parameterNames); }
public ExpressionNode getBody() { return body; }
public boolean isBlock() { return isBlock; }
public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; }
public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) {
this.scopeRef = scope;
}
@Override
public TypeInfo resolveType(TypeResolverContext resolver) {
// Lambda type is determined by the expected functional interface type
// See ExpressionTypeResolver.resolveLambdaType()
return TypeInfo.fromClass(Object.class);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (parameterNames.size() == 1 && !isBlock) {
sb.append(parameterNames.get(0));
} else {
sb.append("(");
sb.append(String.join(", ", parameterNames));
sb.append(")");
}
sb.append(" -> ");
if (isBlock) {
sb.append("{ ... }");
} else {
sb.append(body);
}
return sb.toString();
}
}
public static class JSFunctionNode extends ExpressionNode {
private final String name; // Optional name for named function expressions
private final List<String> parameterNames;
private final String bodyText; // Raw body text (for block functions)
private noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scopeRef;
public JSFunctionNode(String name, List<String> parameterNames, String bodyText, int start, int end) {
super(start, end);
this.name = name;
this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>();
this.bodyText = bodyText;
this.scopeRef = null;
}
public String getName() { return name; }
public List<String> getParameterNames() { return Collections.unmodifiableList(parameterNames); }
public String getBodyText() { return bodyText; }
public noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope getScopeRef() { return scopeRef; }
public void setScopeRef(noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope scope) {
this.scopeRef = scope;
}
@Override
public TypeInfo resolveType(TypeResolverContext resolver) {
// JS function type is determined by the expected functional interface type
// See ExpressionTypeResolver.resolveJSFunctionType()
return TypeInfo.fromClass(Object.class);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("function");
if (name != null && !name.isEmpty()) {
sb.append(" ").append(name);
}
sb.append("(");
sb.append(String.join(", ", parameterNames));
sb.append(") { ");
if (bodyText.length() > 30) {
sb.append(bodyText.substring(0, 30)).append("...");
} else {
sb.append(bodyText);
}
sb.append(" }");
return sb.toString();
}
}
public static class JSArrowNode extends ExpressionNode {
private final List<String> parameterNames;
private final ExpressionNode body;
private final boolean isBlock;
private InnerCallableScope scopeRef;
public JSArrowNode(List<String> parameterNames, ExpressionNode body, boolean isBlock, int start, int end) {
super(start, end);
this.parameterNames = parameterNames != null ? new ArrayList<>(parameterNames) : new ArrayList<>();
this.body = body;
this.isBlock = isBlock;
}
public List<String> getParameterNames() { return Collections.unmodifiableList(parameterNames); }
public ExpressionNode getBody() { return body; }
public boolean isBlock() { return isBlock; }
public InnerCallableScope getScopeRef() { return scopeRef; }
public void setScopeRef(InnerCallableScope scope) {
this.scopeRef = scope;
}
@Override
public TypeInfo resolveType(TypeResolverContext resolver) {
return TypeInfo.fromClass(Object.class);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("(");
sb.append(String.join(", ", parameterNames));
sb.append(") => ");
if (isBlock) {
sb.append("{ ... }");
} else if (body != null) {
sb.append(body);
}
return sb.toString();
}
}
public static class MethodReferenceNode extends ExpressionNode {
private final ExpressionNode target; // Object or class name
private final String methodName;
private final boolean isStatic; // Whether it's Class::method vs obj::method
public MethodReferenceNode(ExpressionNode target, String methodName, boolean isStatic, int start, int end) {
super(start, end);
this.target = target;
this.methodName = methodName;
this.isStatic = isStatic;
}
public ExpressionNode getTarget() { return target; }
public String getMethodName() { return methodName; }
public boolean isStatic() { return isStatic; }
@Override
public TypeInfo resolveType(TypeResolverContext resolver) {
// Method reference type is determined by the expected functional interface type
// See ExpressionTypeResolver.resolveMethodReferenceType()
return TypeInfo.fromClass(Object.class);
}
@Override
public String toString() {
return target + "::" + methodName;
}
}
public interface TypeResolverContext {
TypeInfo resolveIdentifier(String name);
TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName);
TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes);
TypeInfo resolveArrayAccess(TypeInfo arrayType);
TypeInfo resolveTypeName(String typeName);
}
}
@@ -0,0 +1,559 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
import java.util.ArrayList;
import java.util.List;
public class ExpressionParser {
private final List<ExpressionToken> tokens;
private int pos;
public ExpressionParser(List<ExpressionToken> tokens) {
this.tokens = tokens;
this.pos = 0;
}
public ExpressionNode parse() {
if (tokens.isEmpty()) return null;
return parseLambdaOrExpression();
}
private ExpressionNode parseLambdaOrExpression() {
// Check if this is a lambda: (params) -> or param ->
// Look ahead to see if there's a -> after the next identifier/parens
int checkpoint = pos;
List<String> paramNames = null;
int lambdaStart = current().getStart();
boolean isJSArrow = false;
// Try to parse parameter list
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
// Single param without parens
String paramName = current().getText();
advance();
if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) {
paramNames = new ArrayList<>();
paramNames.add(paramName);
} else if (check(ExpressionToken.TokenKind.JS_ARROW)) {
paramNames = new ArrayList<>();
paramNames.add(paramName);
isJSArrow = true;
} else {
// Not a lambda, rewind
pos = checkpoint;
}
} else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) {
// Parenthesized params: (a, b) or ()
advance(); // consume '('
paramNames = new ArrayList<>();
if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
// Parse param list
while (true) {
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) {
// Not a valid lambda param list, rewind
pos = checkpoint;
paramNames = null;
break;
}
paramNames.add(current().getText());
advance();
if (check(ExpressionToken.TokenKind.COMMA)) {
advance();
} else if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
break;
} else {
// Invalid syntax, rewind
pos = checkpoint;
paramNames = null;
break;
}
}
}
if (paramNames != null) {
if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
advance(); // consume ')'
if (check(ExpressionToken.TokenKind.LAMBDA_ARROW)) {
// Valid lambda!
} else if (check(ExpressionToken.TokenKind.JS_ARROW)) {
isJSArrow = true;
} else {
// Not a lambda, rewind
pos = checkpoint;
paramNames = null;
}
} else {
// No closing paren, rewind
pos = checkpoint;
paramNames = null;
}
}
}
if (paramNames != null) {
// This is a lambda! Parse it
advance(); // consume '->' or '=>'
// Parse body
ExpressionNode body;
boolean isBlock = false;
int bodyEnd;
if (check(ExpressionToken.TokenKind.LEFT_BRACE)) {
// Block lambda: { ... }
isBlock = true;
int braceStart = current().getStart();
advance(); // consume '{'
// Skip to matching '}'
int depth = 1;
while (depth > 0 && pos < tokens.size() && !check(ExpressionToken.TokenKind.EOF)) {
if (check(ExpressionToken.TokenKind.LEFT_BRACE)) depth++;
else if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) depth--;
if (depth > 0) advance();
}
bodyEnd = current().getEnd();
if (check(ExpressionToken.TokenKind.RIGHT_BRACE)) {
advance(); // consume '}'
}
body = new ExpressionNode.StringLiteralNode("<block>", braceStart, bodyEnd); // Placeholder
} else {
// Expression lambda
body = parseExpressionInternal(0);
if (body == null) {
// Failed to parse body, return error recovery
return null;
}
bodyEnd = body.getEnd();
}
// Create lambda node (scopeRef will be set during type resolution)
if (isJSArrow)
return new ExpressionNode.JSArrowNode(paramNames, body, isBlock, lambdaStart, bodyEnd);
return new ExpressionNode.LambdaNode(paramNames, body, isBlock, lambdaStart, bodyEnd);
}
// Not a lambda, parse as regular expression
return parseExpressionInternal(0);
}
private ExpressionToken current() {
if (pos >= tokens.size()) return tokens.get(tokens.size() - 1);
return tokens.get(pos);
}
private ExpressionToken advance() {
ExpressionToken tok = current();
if (pos < tokens.size()) pos++;
return tok;
}
private boolean check(ExpressionToken.TokenKind kind) { return current().getKind() == kind; }
private boolean match(ExpressionToken.TokenKind kind) {
if (check(kind)) { advance(); return true; }
return false;
}
private ExpressionNode parseExpression(int minPrecedence) {
// For top-level expressions (minPrecedence = 0), check for lambda first
if (minPrecedence == 0) {
return parseLambdaOrExpression();
}
return parseExpressionInternal(minPrecedence);
}
private ExpressionNode parseExpressionInternal(int minPrecedence) {
ExpressionNode left = parsePrefixExpression();
if (left == null) return null;
while (true) {
if (check(ExpressionToken.TokenKind.QUESTION) && minPrecedence <= 2) {
left = parseTernary(left);
continue;
}
OperatorType op = getCurrentBinaryOperator();
if (op == null) break;
int precedence = op.getPrecedence();
if (precedence < minPrecedence) break;
advance();
int nextMinPrecedence = (op.getAssociativity() == OperatorType.Associativity.LEFT)
? precedence + 1 : precedence;
ExpressionNode right = parseExpression(nextMinPrecedence);
if (right == null) break;
if (op == OperatorType.INSTANCEOF) {
String typeName = extractTypeName(right);
left = new ExpressionNode.InstanceofNode(left, typeName, left.getStart(), right.getEnd());
} else if (op.isAssignment()) {
left = new ExpressionNode.AssignmentNode(left, op, right, left.getStart(), right.getEnd());
} else {
left = new ExpressionNode.BinaryOpNode(left, op, right, left.getStart(), right.getEnd());
}
}
return parsePostfixExpression(left);
}
private String extractTypeName(ExpressionNode node) {
if (node instanceof ExpressionNode.IdentifierNode) {
return ((ExpressionNode.IdentifierNode) node).getName();
}
if (node instanceof ExpressionNode.MemberAccessNode) {
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node;
return extractTypeName(ma.getTarget()) + "." + ma.getMemberName();
}
return "Object";
}
private ExpressionNode parseTernary(ExpressionNode condition) {
int start = condition.getStart();
advance();
ExpressionNode thenExpr = parseExpression(0);
if (thenExpr == null) return condition;
if (!match(ExpressionToken.TokenKind.COLON)) return condition;
ExpressionNode elseExpr = parseExpression(2);
if (elseExpr == null) return condition;
return new ExpressionNode.TernaryNode(condition, thenExpr, elseExpr, start, elseExpr.getEnd());
}
private OperatorType getCurrentBinaryOperator() {
ExpressionToken tok = current();
if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) {
OperatorType op = tok.getOperatorType();
if (op != null && (op.isBinary() || op.isAssignment())) return op;
}
if (tok.getKind() == ExpressionToken.TokenKind.INSTANCEOF) return OperatorType.INSTANCEOF;
return null;
}
private ExpressionNode parsePrefixExpression() {
ExpressionToken tok = current();
int start = tok.getStart();
// Check for function expression (JS only)
if (tok.getKind() == ExpressionToken.TokenKind.FUNCTION) {
return parseFunctionExpression();
}
if (tok.getKind() == ExpressionToken.TokenKind.OPERATOR) {
OperatorType op = tok.getOperatorType();
if (op != null && isUnaryOperator(op)) {
advance();
ExpressionNode operand = parsePrefixExpression();
if (operand == null) return null;
OperatorType unaryOp = toUnaryOperator(op);
return new ExpressionNode.UnaryOpNode(unaryOp, operand, true, start, operand.getEnd());
}
}
if (tok.getKind() == ExpressionToken.TokenKind.LEFT_PAREN) {
return parseCastOrParenthesized();
}
return parsePrimaryExpression();
}
private ExpressionNode parseFunctionExpression() {
int start = current().getStart();
advance(); // consume 'function'
// Optional function name
String functionName = null;
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
functionName = current().getText();
advance();
}
// Expect '('
if (!check(ExpressionToken.TokenKind.LEFT_PAREN)) {
return null; // Invalid syntax
}
advance(); // consume '('
// Parse parameters
List<String> params = new ArrayList<>();
while (!check(ExpressionToken.TokenKind.RIGHT_PAREN) && !check(ExpressionToken.TokenKind.EOF)) {
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) {
return null; // Invalid parameter
}
params.add(current().getText());
advance();
if (check(ExpressionToken.TokenKind.COMMA)) {
advance();
} else if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
return null; // Expected comma or closing paren
}
}
if (!check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
return null; // Expected closing paren
}
advance(); // consume ')'
// Expect '{'
if (!check(ExpressionToken.TokenKind.LEFT_BRACE)) {
return null; // Invalid syntax
}
advance(); // consume '{'
// Collect body tokens until matching '}'
StringBuilder bodyBuilder = new StringBuilder();
int depth = 1;
while (depth > 0 && !check(ExpressionToken.TokenKind.EOF)) {
ExpressionToken bodyToken = current();
if (bodyToken.getKind() == ExpressionToken.TokenKind.LEFT_BRACE) {
depth++;
} else if (bodyToken.getKind() == ExpressionToken.TokenKind.RIGHT_BRACE) {
depth--;
if (depth == 0) {
break;
}
}
bodyBuilder.append(bodyToken.getText()).append(" ");
advance();
}
String bodyText = bodyBuilder.toString().trim();
int end = current().getEnd();
if (!check(ExpressionToken.TokenKind.RIGHT_BRACE)) {
return null; // Expected closing brace
}
advance(); // consume final '}'
// Find the corresponding InnerCallableScope from ScriptDocument
// This will be set during type resolution when we have access to ScriptDocument
return new ExpressionNode.JSFunctionNode(functionName, params, bodyText, start, end);
}
private boolean isUnaryOperator(OperatorType op) {
switch (op) {
case ADD: case SUBTRACT: case LOGICAL_NOT: case BITWISE_NOT:
case PRE_INCREMENT: case PRE_DECREMENT: return true;
default: return false;
}
}
private OperatorType toUnaryOperator(OperatorType op) {
switch (op) {
case ADD: return OperatorType.UNARY_PLUS;
case SUBTRACT: return OperatorType.UNARY_MINUS;
default: return op;
}
}
private ExpressionNode parseCastOrParenthesized() {
int start = current().getStart();
advance();
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
String possibleType = current().getText();
int savedPos = pos;
advance();
while (check(ExpressionToken.TokenKind.DOT)) {
advance();
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
possibleType += "." + current().getText();
advance();
}
}
while (check(ExpressionToken.TokenKind.LEFT_BRACKET)) {
advance();
if (!match(ExpressionToken.TokenKind.RIGHT_BRACKET)) { pos = savedPos; break; }
possibleType += "[]";
}
if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) {
advance();
if (canStartExpression()) {
ExpressionNode expr = parsePrefixExpression();
if (expr != null) {
return new ExpressionNode.CastNode(possibleType, expr, start, expr.getEnd());
}
}
}
pos = savedPos;
}
ExpressionNode inner = parseExpression(0);
if (inner != null && match(ExpressionToken.TokenKind.RIGHT_PAREN)) {
ExpressionNode paren = new ExpressionNode.ParenthesizedNode(inner, start, current().getStart());
return parseAccessChain(paren);
}
return inner;
}
private boolean canStartExpression() {
ExpressionToken.TokenKind kind = current().getKind();
switch (kind) {
case IDENTIFIER: case INT_LITERAL: case LONG_LITERAL: case FLOAT_LITERAL:
case DOUBLE_LITERAL: case BOOLEAN_LITERAL: case CHAR_LITERAL: case STRING_LITERAL:
case NULL_LITERAL: case NEW: case LEFT_PAREN: case OPERATOR: return true;
default: return false;
}
}
private ExpressionNode parsePrimaryExpression() {
ExpressionToken tok = current();
int start = tok.getStart();
switch (tok.getKind()) {
case INT_LITERAL: advance(); return new ExpressionNode.IntLiteralNode(tok.getText(), start, tok.getEnd());
case LONG_LITERAL: advance(); return new ExpressionNode.LongLiteralNode(tok.getText(), start, tok.getEnd());
case FLOAT_LITERAL: advance(); return new ExpressionNode.FloatLiteralNode(tok.getText(), start, tok.getEnd());
case DOUBLE_LITERAL: advance(); return new ExpressionNode.DoubleLiteralNode(tok.getText(), start, tok.getEnd());
case BOOLEAN_LITERAL: advance(); return new ExpressionNode.BooleanLiteralNode("true".equals(tok.getText()), start, tok.getEnd());
case CHAR_LITERAL: advance(); return new ExpressionNode.CharLiteralNode(tok.getText(), start, tok.getEnd());
case STRING_LITERAL: advance(); return new ExpressionNode.StringLiteralNode(tok.getText(), start, tok.getEnd());
case NULL_LITERAL: advance(); return new ExpressionNode.NullLiteralNode(start, tok.getEnd());
case NEW: return parseNewExpression();
case IDENTIFIER: return parseIdentifierOrMethodCall();
case LEFT_PAREN: return parseCastOrParenthesized();
default: return null;
}
}
private ExpressionNode parseNewExpression() {
int start = current().getStart();
advance();
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) return null;
StringBuilder typeName = new StringBuilder(current().getText());
advance();
while (check(ExpressionToken.TokenKind.DOT)) {
advance();
if (check(ExpressionToken.TokenKind.IDENTIFIER)) {
typeName.append(".").append(current().getText());
advance();
}
}
List<ExpressionNode> args = new ArrayList<>();
if (match(ExpressionToken.TokenKind.LEFT_PAREN)) {
args = parseArgumentList();
match(ExpressionToken.TokenKind.RIGHT_PAREN);
}
return new ExpressionNode.NewNode(typeName.toString(), args, start, current().getStart());
}
private ExpressionNode parseIdentifierOrMethodCall() {
int start = current().getStart();
String name = current().getText();
advance();
ExpressionNode result = new ExpressionNode.IdentifierNode(name, start, current().getStart());
return parseAccessChain(result);
}
private ExpressionNode parseAccessChain(ExpressionNode base) {
while (true) {
if (check(ExpressionToken.TokenKind.METHOD_REFERENCE)) {
advance(); // consume '::'
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) {
// Invalid syntax, return what we have
break;
}
String methodName = current().getText();
int end = current().getEnd();
advance();
// isStatic is determined during type resolution, not parsing
// We pass false as a placeholder - ExpressionTypeResolver will compute
// the real value based on whether target resolves to a ClassTypeInfo
base = new ExpressionNode.MethodReferenceNode(base, methodName, false, base.getStart(), end);
// Method references don't chain further (can't do obj::method.something)
break;
} else if (check(ExpressionToken.TokenKind.LEFT_PAREN)) {
advance();
List<ExpressionNode> args = parseArgumentList();
int end = current().getStart();
match(ExpressionToken.TokenKind.RIGHT_PAREN);
String methodName;
ExpressionNode target;
if (base instanceof ExpressionNode.IdentifierNode) {
methodName = ((ExpressionNode.IdentifierNode) base).getName();
target = null;
} else if (base instanceof ExpressionNode.MemberAccessNode) {
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) base;
methodName = ma.getMemberName();
target = ma.getTarget();
} else {
methodName = "apply";
target = base;
}
base = new ExpressionNode.MethodCallNode(target, methodName, args, base.getStart(), end);
} else if (check(ExpressionToken.TokenKind.DOT)) {
advance();
if (!check(ExpressionToken.TokenKind.IDENTIFIER)) break;
String memberName = current().getText();
int end = current().getEnd();
advance();
base = new ExpressionNode.MemberAccessNode(base, memberName, base.getStart(), end);
} else if (check(ExpressionToken.TokenKind.LEFT_BRACKET)) {
advance();
ExpressionNode index = parseExpression(0);
int end = current().getStart();
match(ExpressionToken.TokenKind.RIGHT_BRACKET);
if (index != null) {
base = new ExpressionNode.ArrayAccessNode(base, index, base.getStart(), end);
}
} else {
break;
}
}
return base;
}
private List<ExpressionNode> parseArgumentList() {
List<ExpressionNode> args = new ArrayList<>();
if (check(ExpressionToken.TokenKind.RIGHT_PAREN)) return args;
ExpressionNode first = parseExpression(0);
if (first != null) args.add(first);
while (match(ExpressionToken.TokenKind.COMMA)) {
ExpressionNode arg = parseExpression(0);
if (arg != null) args.add(arg);
}
return args;
}
private ExpressionNode parsePostfixExpression(ExpressionNode expr) {
if (expr == null) return null;
while (true) {
ExpressionToken tok = current();
if (tok.getKind() != ExpressionToken.TokenKind.OPERATOR) break;
OperatorType op = tok.getOperatorType();
if (op == OperatorType.PRE_INCREMENT) {
advance();
expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_INCREMENT, expr, false, expr.getStart(), tok.getEnd());
} else if (op == OperatorType.PRE_DECREMENT) {
advance();
expr = new ExpressionNode.UnaryOpNode(OperatorType.POST_DECREMENT, expr, false, expr.getStart(), tok.getEnd());
} else {
break;
}
}
return expr;
}
}
@@ -0,0 +1,65 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
public class ExpressionToken {
public enum TokenKind {
INT_LITERAL, LONG_LITERAL, FLOAT_LITERAL, DOUBLE_LITERAL,
BOOLEAN_LITERAL, CHAR_LITERAL, STRING_LITERAL, NULL_LITERAL,
IDENTIFIER, NEW, INSTANCEOF, FUNCTION,
OPERATOR,
LEFT_PAREN, RIGHT_PAREN, LEFT_BRACKET, RIGHT_BRACKET,
LEFT_BRACE, RIGHT_BRACE,
DOT, COMMA, QUESTION, COLON, SEMICOLON,
LAMBDA_ARROW, JS_ARROW, METHOD_REFERENCE,
EOF
}
private final TokenKind kind;
private final String text;
private final int start;
private final int end;
private final OperatorType operatorType;
public ExpressionToken(TokenKind kind, String text, int start, int end) {
this(kind, text, start, end, null);
}
public ExpressionToken(TokenKind kind, String text, int start, int end, OperatorType operatorType) {
this.kind = kind;
this.text = text;
this.start = start;
this.end = end;
this.operatorType = operatorType;
}
public TokenKind getKind() { return kind; }
public String getText() { return text; }
public int getStart() { return start; }
public int getEnd() { return end; }
public OperatorType getOperatorType() { return operatorType; }
public static ExpressionToken operator(String symbol, int start, int end) {
OperatorType op = OperatorType.fromBinarySymbol(symbol);
if (op == null) op = OperatorType.fromSymbol(symbol);
return new ExpressionToken(TokenKind.OPERATOR, symbol, start, end, op);
}
public static ExpressionToken identifier(String name, int start, int end) {
if ("true".equals(name) || "false".equals(name)) {
return new ExpressionToken(TokenKind.BOOLEAN_LITERAL, name, start, end);
}
if ("null".equals(name)) {
return new ExpressionToken(TokenKind.NULL_LITERAL, name, start, end);
}
if ("new".equals(name)) {
return new ExpressionToken(TokenKind.NEW, name, start, end);
}
if ("instanceof".equals(name)) {
return new ExpressionToken(TokenKind.INSTANCEOF, name, start, end);
}
if ("function".equals(name)) {
return new ExpressionToken(TokenKind.FUNCTION, name, start, end);
}
return new ExpressionToken(TokenKind.IDENTIFIER, name, start, end);
}
}
@@ -0,0 +1,176 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
import java.util.ArrayList;
import java.util.List;
public class ExpressionTokenizer {
public static List<ExpressionToken> tokenize(String expr) {
List<ExpressionToken> tokens = new ArrayList<>();
int pos = 0;
int len = expr.length();
while (pos < len) {
char c = expr.charAt(pos);
if (Character.isWhitespace(c)) { pos++; continue; }
if (c == '"') {
int start = pos++;
while (pos < len && expr.charAt(pos) != '"') {
if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++;
pos++;
}
if (pos < len) pos++;
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.STRING_LITERAL, expr.substring(start, pos), start, pos));
continue;
}
if (c == '\'') {
int start = pos++;
while (pos < len && expr.charAt(pos) != '\'') {
if (expr.charAt(pos) == '\\' && pos + 1 < len) pos++;
pos++;
}
if (pos < len) pos++;
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.CHAR_LITERAL, expr.substring(start, pos), start, pos));
continue;
}
if (Character.isDigit(c) || (c == '.' && pos + 1 < len && Character.isDigit(expr.charAt(pos + 1)))) {
int start = pos;
if (c == '0' && pos + 1 < len) {
char next = expr.charAt(pos + 1);
if (next == 'x' || next == 'X') {
pos += 2;
while (pos < len && isHexDigit(expr.charAt(pos))) pos++;
if (pos < len && (expr.charAt(pos) == 'L' || expr.charAt(pos) == 'l')) {
pos++;
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LONG_LITERAL, expr.substring(start, pos), start, pos));
} else {
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos));
}
continue;
}
if (next == 'b' || next == 'B') {
pos += 2;
while (pos < len && (expr.charAt(pos) == '0' || expr.charAt(pos) == '1')) pos++;
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.INT_LITERAL, expr.substring(start, pos), start, pos));
continue;
}
}
while (pos < len && Character.isDigit(expr.charAt(pos))) pos++;
boolean hasDecimal = false;
if (pos < len && expr.charAt(pos) == '.') {
if (pos + 1 < len && Character.isDigit(expr.charAt(pos + 1))) {
hasDecimal = true;
pos++;
while (pos < len && Character.isDigit(expr.charAt(pos))) pos++;
}
}
if (pos < len && (expr.charAt(pos) == 'e' || expr.charAt(pos) == 'E')) {
pos++;
if (pos < len && (expr.charAt(pos) == '+' || expr.charAt(pos) == '-')) pos++;
while (pos < len && Character.isDigit(expr.charAt(pos))) pos++;
hasDecimal = true;
}
ExpressionToken.TokenKind kind = ExpressionToken.TokenKind.INT_LITERAL;
if (pos < len) {
char suffix = expr.charAt(pos);
if (suffix == 'f' || suffix == 'F') { kind = ExpressionToken.TokenKind.FLOAT_LITERAL; pos++; }
else if (suffix == 'd' || suffix == 'D') { kind = ExpressionToken.TokenKind.DOUBLE_LITERAL; pos++; }
else if (suffix == 'L' || suffix == 'l') { kind = ExpressionToken.TokenKind.LONG_LITERAL; pos++; }
else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL;
} else if (hasDecimal) kind = ExpressionToken.TokenKind.DOUBLE_LITERAL;
tokens.add(new ExpressionToken(kind, expr.substring(start, pos), start, pos));
continue;
}
if (Character.isJavaIdentifierStart(c)) {
int start = pos;
while (pos < len && Character.isJavaIdentifierPart(expr.charAt(pos))) pos++;
tokens.add(ExpressionToken.identifier(expr.substring(start, pos), start, pos));
continue;
}
int start = pos;
String op = null;
// Check for method reference :: (before lambda arrow and single colon)
if (pos + 1 < len && c == ':' && expr.charAt(pos + 1) == ':') {
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.METHOD_REFERENCE, "::", pos, pos + 2));
pos += 2;
continue;
}
// Check for lambda arrow (Java ->)
if (pos + 1 < len && c == '-' && expr.charAt(pos + 1) == '>') {
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LAMBDA_ARROW, "->", pos, pos + 2));
pos += 2;
continue;
}
// Check for JS arrow (=>)
if (pos + 1 < len && c == '=' && expr.charAt(pos + 1) == '>') {
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.JS_ARROW, "=>", pos, pos + 2));
pos += 2;
continue;
}
if (pos + 3 <= len) {
String s3 = expr.substring(pos, pos + 3);
if (">>>".equals(s3) || "<<=".equals(s3) || ">>=".equals(s3)) op = s3;
}
if (op == null && pos + 4 <= len) {
String s4 = expr.substring(pos, pos + 4);
if (">>>=".equals(s4)) op = s4;
}
if (op == null && pos + 2 <= len) {
String s2 = expr.substring(pos, pos + 2);
if ("++".equals(s2) || "--".equals(s2) || "+=".equals(s2) || "-=".equals(s2) ||
"*=".equals(s2) || "/=".equals(s2) || "%=".equals(s2) || "&=".equals(s2) ||
"|=".equals(s2) || "^=".equals(s2) || "==".equals(s2) || "!=".equals(s2) ||
"<=".equals(s2) || ">=".equals(s2) || "&&".equals(s2) || "||".equals(s2) ||
"<<".equals(s2) || ">>".equals(s2)) op = s2;
}
if (op == null) {
switch (c) {
case '+': case '-': case '*': case '/': case '%': case '&': case '|':
case '^': case '~': case '!': case '<': case '>': case '=':
op = String.valueOf(c); break;
}
}
if (op != null) {
pos += op.length();
tokens.add(ExpressionToken.operator(op, start, pos));
continue;
}
switch (c) {
case '(': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_PAREN, "(", pos, pos + 1)); break;
case ')': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_PAREN, ")", pos, pos + 1)); break;
case '[': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACKET, "[", pos, pos + 1)); break;
case ']': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACKET, "]", pos, pos + 1)); break;
case '{': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.LEFT_BRACE, "{", pos, pos + 1)); break;
case '}': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.RIGHT_BRACE, "}", pos, pos + 1)); break;
case '.': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.DOT, ".", pos, pos + 1)); break;
case ',': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COMMA, ",", pos, pos + 1)); break;
case '?': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.QUESTION, "?", pos, pos + 1)); break;
case ':': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.COLON, ":", pos, pos + 1)); break;
case ';': tokens.add(new ExpressionToken(ExpressionToken.TokenKind.SEMICOLON, ";", pos, pos + 1)); break;
}
pos++;
}
tokens.add(new ExpressionToken(ExpressionToken.TokenKind.EOF, "", len, len));
return tokens;
}
private static boolean isHexDigit(char c) {
return Character.isDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
}
}
@@ -0,0 +1,949 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
import noppes.npcs.client.gui.util.script.interpreter.InnerCallableScope;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.ClassTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.OverloadSelector;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.ArrayList;
import java.util.List;
public class ExpressionTypeResolver {
private final ExpressionNode.TypeResolverContext context;
private final ScriptDocument document; // Access to document for inner scope lookups
private final int basePosition; // Document offset for this expression
/**
* Static field to track the expected/desired type for the current expression being resolved.
* This allows TypeRules to validate type compatibility in context (e.g., ternary operator branches
* must be compatible with the assignment target type).
* Should be set before calling resolve() and cleared after.
*/
public static TypeInfo CURRENT_EXPECTED_TYPE = null;
public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context) {
this.context = context;
this.document = null;
this.basePosition = 0;
}
public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document) {
this(context, document, 0);
}
public ExpressionTypeResolver(ExpressionNode.TypeResolverContext context, ScriptDocument document, int basePosition) {
this.context = context;
this.document = document;
this.basePosition = basePosition;
}
public TypeInfo resolve(String expression) {
if (expression == null || expression.trim().isEmpty()) return null;
try {
List<ExpressionToken> tokens = ExpressionTokenizer.tokenize(expression);
if (tokens.isEmpty()) return null;
ExpressionParser parser = new ExpressionParser(tokens);
ExpressionNode ast = parser.parse();
if (ast == null) return null;
return resolveNodeType(ast);
} catch (Exception e) {
return null;
}
}
public static boolean containsOperators(String expression) {
if (expression == null) return false;
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
switch (c) {
case '+': case '-': case '*': case '/': case '%':
case '&': case '|': case '^': case '~':
case '<': case '>': case '=': case '!':
case '?': case ':': return true;
}
}
return false;
}
private TypeInfo resolveNodeType(ExpressionNode node) {
if (node == null) return null;
if (node instanceof ExpressionNode.IntLiteralNode) {
if (CURRENT_EXPECTED_TYPE != null) {
TypeInfo narrowed = TypeChecker.narrowLiteralToExpectedType(
((ExpressionNode.IntLiteralNode) node).getValue(), CURRENT_EXPECTED_TYPE);
if (narrowed != null) return narrowed;
}
return TypeInfo.fromPrimitive("int");
}
if (node instanceof ExpressionNode.LongLiteralNode) return TypeInfo.fromPrimitive("long");
if (node instanceof ExpressionNode.FloatLiteralNode) return TypeInfo.fromPrimitive("float");
if (node instanceof ExpressionNode.DoubleLiteralNode) return TypeInfo.fromPrimitive("double");
if (node instanceof ExpressionNode.BooleanLiteralNode) return TypeInfo.fromPrimitive("boolean");
if (node instanceof ExpressionNode.CharLiteralNode) return TypeInfo.fromPrimitive("char");
if (node instanceof ExpressionNode.StringLiteralNode) return TypeInfo.string();
if (node instanceof ExpressionNode.NullLiteralNode) return TypeInfo.unresolved("null", "<null>");
if (node instanceof ExpressionNode.IdentifierNode) {
return context.resolveIdentifier(((ExpressionNode.IdentifierNode) node).getName());
}
if (node instanceof ExpressionNode.MemberAccessNode) {
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node;
TypeInfo targetType = resolveNodeType(ma.getTarget());
if (targetType == null || !targetType.isResolved()) return null;
if (targetType == TypeInfo.ANY || "any".equals(targetType.getFullName())) return TypeInfo.ANY;
return context.resolveMemberAccess(targetType, ma.getMemberName());
}
if (node instanceof ExpressionNode.MethodCallNode) {
ExpressionNode.MethodCallNode mc = (ExpressionNode.MethodCallNode) node;
TypeInfo targetType = mc.getTarget() == null ? context.resolveIdentifier("this") : resolveNodeType(mc.getTarget());
if (targetType == null || !targetType.isResolved()) return null;
if (targetType == TypeInfo.ANY || "any".equals(targetType.getFullName())) return TypeInfo.ANY;
TypeInfo[] argTypes = new TypeInfo[mc.getArguments().size()];
for (int i = 0; i < argTypes.length; i++) argTypes[i] = resolveNodeType(mc.getArguments().get(i));
return context.resolveMethodCall(targetType, mc.getMethodName(), argTypes);
}
if (node instanceof ExpressionNode.ArrayAccessNode) {
ExpressionNode.ArrayAccessNode aa = (ExpressionNode.ArrayAccessNode) node;
TypeInfo arrayType = resolveNodeType(aa.getArray());
if (arrayType == null || !arrayType.isResolved()) return null;
if (arrayType == TypeInfo.ANY || "any".equals(arrayType.getFullName())) return TypeInfo.ANY;
String typeName = arrayType.getFullName();
if (typeName.endsWith("[]")) {
String elementTypeName = typeName.substring(0, typeName.length() - 2);
return context.resolveTypeName(elementTypeName);
}
return context.resolveArrayAccess(arrayType);
}
if (node instanceof ExpressionNode.NewNode) {
return context.resolveTypeName(((ExpressionNode.NewNode) node).getTypeName());
}
if (node instanceof ExpressionNode.BinaryOpNode) {
ExpressionNode.BinaryOpNode bin = (ExpressionNode.BinaryOpNode) node;
TypeInfo leftType = resolveNodeType(bin.getLeft());
TypeInfo rightType = resolveNodeType(bin.getRight());
return TypeRules.resolveBinaryOperatorType(bin.getOperator(), leftType, rightType);
}
if (node instanceof ExpressionNode.UnaryOpNode) {
ExpressionNode.UnaryOpNode un = (ExpressionNode.UnaryOpNode) node;
// Special case: negation of an IntLiteralNode can narrow to byte/short
if (un.getOperator() == OperatorType.UNARY_MINUS
&& un.getOperand() instanceof ExpressionNode.IntLiteralNode
&& CURRENT_EXPECTED_TYPE != null) {
String negatedText = "-" + ((ExpressionNode.IntLiteralNode) un.getOperand()).getValue();
TypeInfo narrowed = TypeChecker.narrowLiteralToExpectedType(negatedText, CURRENT_EXPECTED_TYPE);
if (narrowed != null) return narrowed;
}
TypeInfo operandType = resolveNodeType(un.getOperand());
return TypeRules.resolveUnaryOperatorType(un.getOperator(), operandType);
}
if (node instanceof ExpressionNode.TernaryNode) {
ExpressionNode.TernaryNode tern = (ExpressionNode.TernaryNode) node;
TypeInfo thenType = resolveNodeType(tern.getThenExpr());
TypeInfo elseType = resolveNodeType(tern.getElseExpr());
return TypeRules.resolveTernaryType(thenType, elseType);
}
if (node instanceof ExpressionNode.CastNode) {
return context.resolveTypeName(((ExpressionNode.CastNode) node).getTypeName());
}
if (node instanceof ExpressionNode.InstanceofNode) {
return TypeInfo.fromPrimitive("boolean");
}
if (node instanceof ExpressionNode.AssignmentNode) {
return resolveNodeType(((ExpressionNode.AssignmentNode) node).getTarget());
}
if (node instanceof ExpressionNode.ParenthesizedNode) {
return resolveNodeType(((ExpressionNode.ParenthesizedNode) node).getInner());
}
if (node instanceof ExpressionNode.LambdaNode) {
return resolveLambdaType((ExpressionNode.LambdaNode) node);
}
if (node instanceof ExpressionNode.JSFunctionNode) {
return resolveJSFunctionType((ExpressionNode.JSFunctionNode) node);
}
if (node instanceof ExpressionNode.JSArrowNode) {
return resolveJSArrowType((ExpressionNode.JSArrowNode) node);
}
if (node instanceof ExpressionNode.MethodReferenceNode) {
return resolveMethodReferenceType((ExpressionNode.MethodReferenceNode) node);
}
return null;
}
private TypeInfo resolveLambdaType(ExpressionNode.LambdaNode lambda) {
// Lambda type is determined by the expected functional interface
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
if (expectedType == null) {
// No expected type context
// Return a generic Object type
return TypeInfo.fromClass(Object.class);
}
// Get SAM method from the expected functional interface
MethodInfo sam = expectedType.getSingleAbstractMethod();
if (sam != null && document != null) {
// Find the InnerCallableScope for this lambda by matching position and parameters
InnerCallableScope scope = findLambdaScopeByPosition(lambda.getStart(), lambda.getParameterNames());
if (scope != null) {
scope.setExpectedType(expectedType);
// Extract and apply parameter types from SAM to lambda parameters
List<FieldInfo> samParams = sam.getParameters();
List<FieldInfo> lambdaParams = scope.getParameters();
if (samParams.size() == lambdaParams.size()) {
for (int i = 0; i < lambdaParams.size(); i++) {
FieldInfo lambdaParam = lambdaParams.get(i);
TypeInfo inferredType = samParams.get(i).getTypeInfo();
// Set inferred type on the parameter
if (inferredType != null) {
lambdaParam.setInferredType(inferredType);
}
}
}
}
// Type check lambda body (if expression lambda)
if (!lambda.isBlock() && lambda.getBody() != null) {
// Resolve body type for validation
TypeInfo bodyType = resolveNodeType(lambda.getBody());
// The actual compatibility check will be done during marking phase
}
return expectedType;
}
// Fallback: check if it's a recognized functional interface by name
String typeName = expectedType.getFullName();
if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") ||
typeName.contains("Predicate") || typeName.contains("Supplier") ||
typeName.equals("java.lang.Runnable"))) {
// Update the scope reference if available
if (lambda.getScopeRef() != null) {
lambda.getScopeRef().setExpectedType(expectedType);
}
return expectedType;
}
// Not a recognized functional interface, return Object
return TypeInfo.fromClass(Object.class);
}
/**
* Find the InnerCallableScope for a lambda by matching position and parameter names.
*
* @param expressionRelativePos The start position of the lambda (relative to expression string)
* @param paramNames The parameter names of the lambda
* @return The matching InnerCallableScope, or null if not found
*/
private InnerCallableScope findLambdaScopeByPosition(int expressionRelativePos, List<String> paramNames) {
if (document == null) {
return null;
}
// Convert expression-relative position to absolute document position
int absolutePos = basePosition + expressionRelativePos;
for (InnerCallableScope scope : document.getInnerScopes()) {
if (scope.getKind() == InnerCallableScope.Kind.JAVA_LAMBDA) {
// Check if absolute position is in range
if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) {
// Verify parameter names match
if (scope.getParameters().size() == paramNames.size()) {
boolean match = true;
for (int i = 0; i < paramNames.size(); i++) {
if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) {
match = false;
break;
}
}
if (match) {
return scope;
}
}
}
}
}
return null;
}
private TypeInfo resolveJSFunctionType(ExpressionNode.JSFunctionNode functionNode) {
// JS function type is determined by the expected functional interface (if any)
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
if (expectedType == null) {
// No expected type - return generic Function type or Object
// Try to resolve "Function" as a type from the context
TypeInfo functionType = context.resolveTypeName("Function");
if (functionType != null && functionType.isResolved()) {
return functionType;
}
return TypeInfo.fromClass(Object.class); // Fallback
}
// Get SAM method from the expected functional interface
MethodInfo sam = expectedType.getSingleAbstractMethod();
if (sam != null && document != null) {
// Find the InnerCallableScope for this JS function by matching position and parameters
InnerCallableScope scope = findJSFunctionScopeByPosition(functionNode.getStart(), functionNode.getParameterNames());
if (scope != null) {
scope.setExpectedType(expectedType);
// Extract and apply parameter types from SAM to function parameters
List<FieldInfo> samParams = sam.getParameters();
List<FieldInfo> functionParams = scope.getParameters();
if (samParams.size() == functionParams.size()) {
for (int i = 0; i < functionParams.size(); i++) {
FieldInfo functionParam = functionParams.get(i);
TypeInfo inferredType = samParams.get(i).getTypeInfo();
// Set inferred type on the parameter
if (inferredType != null) {
functionParam.setInferredType(inferredType);
}
}
}
}
return expectedType;
}
// Fallback: check if expected type is compatible with a function
String typeName = expectedType.getFullName();
if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") ||
typeName.contains("Predicate") || typeName.contains("Supplier") ||
typeName.equals("java.lang.Runnable"))) {
// This is likely a functional interface
// Update the InnerCallableScope with the expected type
if (functionNode.getScopeRef() != null) {
functionNode.getScopeRef().setExpectedType(expectedType);
}
return expectedType;
}
// Expected type is not a recognized functional interface
// Check if it's a generic JS "Function" type expectation
if (typeName != null && typeName.equals("Function")) {
return expectedType;
}
return TypeInfo.fromClass(Object.class); // Fallback
}
/**
* Find the InnerCallableScope for a JS function by matching position and parameter names.
*
* @param expressionRelativePos The start position of the function (relative to expression string)
* @param paramNames The parameter names of the function
* @return The matching InnerCallableScope, or null if not found
*/
private InnerCallableScope findJSFunctionScopeByPosition(int expressionRelativePos, List<String> paramNames) {
if (document == null) {
return null;
}
// Convert expression-relative position to absolute document position
int absolutePos = basePosition + expressionRelativePos;
for (InnerCallableScope scope : document.getInnerScopes()) {
if (scope.getKind() == InnerCallableScope.Kind.JS_FUNCTION_EXPR) {
// Check if absolute position is in range
if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) {
// Verify parameter names match
if (scope.getParameters().size() == paramNames.size()) {
boolean match = true;
for (int i = 0; i < paramNames.size(); i++) {
if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) {
match = false;
break;
}
}
if (match) {
return scope;
}
}
}
}
}
return null;
}
private TypeInfo resolveJSArrowType(ExpressionNode.JSArrowNode arrowNode) {
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
if (expectedType == null) {
TypeInfo functionType = context.resolveTypeName("Function");
if (functionType != null && functionType.isResolved()) {
return functionType;
}
return TypeInfo.fromClass(Object.class);
}
MethodInfo sam = expectedType.getSingleAbstractMethod();
if (sam != null && document != null) {
InnerCallableScope scope = findJSArrowScopeByPosition(arrowNode.getStart(), arrowNode.getParameterNames());
if (scope != null) {
scope.setExpectedType(expectedType);
List<FieldInfo> samParams = sam.getParameters();
List<FieldInfo> arrowParams = scope.getParameters();
if (samParams.size() == arrowParams.size()) {
for (int i = 0; i < arrowParams.size(); i++) {
TypeInfo inferredType = samParams.get(i).getTypeInfo();
if (inferredType != null) {
arrowParams.get(i).setInferredType(inferredType);
}
}
}
}
return expectedType;
}
String typeName = expectedType.getFullName();
if (typeName != null && (typeName.contains("Function") || typeName.contains("Consumer") ||
typeName.contains("Predicate") || typeName.contains("Supplier") ||
typeName.equals("java.lang.Runnable"))) {
if (arrowNode.getScopeRef() != null) {
arrowNode.getScopeRef().setExpectedType(expectedType);
}
return expectedType;
}
if (typeName != null && typeName.equals("Function")) {
return expectedType;
}
return TypeInfo.fromClass(Object.class);
}
private InnerCallableScope findJSArrowScopeByPosition(int expressionRelativePos, List<String> paramNames) {
if (document == null) {
return null;
}
int absolutePos = basePosition + expressionRelativePos;
for (InnerCallableScope scope : document.getInnerScopes()) {
if (scope.getKind() == InnerCallableScope.Kind.JS_ARROW_FUNC) {
if (absolutePos >= scope.getHeaderStart() && absolutePos < scope.getFullEnd()) {
if (scope.getParameters().size() == paramNames.size()) {
boolean match = true;
for (int i = 0; i < paramNames.size(); i++) {
if (!scope.getParameters().get(i).getName().equals(paramNames.get(i))) {
match = false;
break;
}
}
if (match) {
return scope;
}
}
}
}
}
return null;
}
// ==================== Method Reference Resolution ====================
/**
* Resolve the type of a method reference expression (target::methodName).
*
* Supports all Java method reference forms:
* - this::method - Instance method on current object
* - super::method - Instance method on superclass
* - variable::method - Instance method on a variable's value
* - ClassName::method - Static method OR unbound instance method
* - pkg.ClassName::method - Fully qualified class reference
* - ClassName::new - Constructor reference
* - Type[]::new - Array constructor reference
*
* @param methodRef The method reference AST node
* @return The resolved type (functional interface type if valid, Object if invalid)
*/
private TypeInfo resolveMethodReferenceType(ExpressionNode.MethodReferenceNode methodRef) {
TypeInfo expectedType = CURRENT_EXPECTED_TYPE;
// Must have expected functional interface context
if (expectedType == null || !expectedType.isFunctionalInterface()) {
return TypeInfo.fromClass(Object.class);
}
MethodInfo sam = expectedType.getSingleAbstractMethod();
if (sam == null) {
return TypeInfo.fromClass(Object.class);
}
// Resolve target and determine if it's a class reference (static context)
MethodReferenceTarget resolvedTarget = resolveMethodReferenceTarget(methodRef);
if (resolvedTarget == null || resolvedTarget.type == null) {
// Unresolved target - return expected type to allow forward references
return expectedType;
}
String methodName = methodRef.getMethodName();
// Handle constructor references (ClassName::new or Type[]::new)
if ("new".equals(methodName)) {
return resolveConstructorReference(resolvedTarget, sam, expectedType);
}
// Handle method references
return resolveMethodReference(resolvedTarget, methodName, sam, expectedType);
}
/**
* Encapsulates the resolved target of a method reference.
*/
private static class MethodReferenceTarget {
final TypeInfo type; // The resolved type
final boolean isClassRef; // True if target is a class reference (for static access)
MethodReferenceTarget(TypeInfo type, boolean isClassRef) {
this.type = type;
this.isClassRef = isClassRef;
}
}
/**
* Resolve the target (left side of ::) for a method reference.
* Returns both the type and whether it's a class reference.
*/
private MethodReferenceTarget resolveMethodReferenceTarget(ExpressionNode.MethodReferenceNode methodRef) {
ExpressionNode target = methodRef.getTarget();
if (target == null) {
return null;
}
// Handle simple identifiers: this, super, variable, ClassName
if (target instanceof ExpressionNode.IdentifierNode) {
String name = ((ExpressionNode.IdentifierNode) target).getName();
return resolveSimpleTarget(name);
}
// Handle qualified names: pkg.ClassName or outer.Inner
if (target instanceof ExpressionNode.MemberAccessNode) {
return resolveQualifiedTarget((ExpressionNode.MemberAccessNode) target);
}
// Handle array type targets: Type[]
if (target instanceof ExpressionNode.ArrayAccessNode) {
TypeInfo arrayType = resolveNodeType(target);
if (arrayType != null && arrayType.isResolved()) {
// Array type is always a class reference (for Type[]::new)
return new MethodReferenceTarget(arrayType, true);
}
}
// Fallback: resolve as expression (e.g., (expr)::method)
TypeInfo exprType = resolveNodeType(target);
if (exprType != null && exprType.isResolved()) {
// Expression results are always instance references
return new MethodReferenceTarget(exprType, false);
}
return null;
}
/**
* Resolve a simple identifier as method reference target.
*/
private MethodReferenceTarget resolveSimpleTarget(String name) {
// this::method - instance method on current object
if ("this".equals(name)) {
TypeInfo thisType = context.resolveIdentifier("this");
return thisType != null ? new MethodReferenceTarget(thisType, false) : null;
}
// super::method - instance method on superclass
if ("super".equals(name)) {
TypeInfo superType = context.resolveIdentifier("super");
return superType != null ? new MethodReferenceTarget(superType, false) : null;
}
// Try as type name first (ClassName::method)
TypeInfo typeInfo = context.resolveTypeName(name);
if (typeInfo != null && typeInfo.isResolved()) {
// Wrap as ClassTypeInfo to indicate class reference
TypeInfo classRef = new ClassTypeInfo(typeInfo);
return new MethodReferenceTarget(classRef, true);
}
// Try as variable (instance::method)
TypeInfo varType = context.resolveIdentifier(name);
if (varType != null && varType.isResolved()) {
// Check if variable holds a Class reference (e.g., var File = Java.type("java.io.File"))
boolean isClassRef = varType.isClassReference();
return new MethodReferenceTarget(varType, isClassRef);
}
return null;
}
/**
* Resolve a qualified name (a.b.c) as method reference target.
* Handles both package-qualified class names and nested class access.
*/
private MethodReferenceTarget resolveQualifiedTarget(ExpressionNode.MemberAccessNode memberAccess) {
// Try to build qualified name and resolve as type
String qualifiedName = buildQualifiedName(memberAccess);
if (qualifiedName != null) {
TypeInfo typeInfo = context.resolveTypeName(qualifiedName);
if (typeInfo != null && typeInfo.isResolved()) {
TypeInfo classRef = new ClassTypeInfo(typeInfo);
return new MethodReferenceTarget(classRef, true);
}
}
// Try resolving as expression chain (object.field::method)
TypeInfo exprType = resolveNodeType(memberAccess);
if (exprType != null && exprType.isResolved()) {
boolean isClassRef = exprType.isClassReference();
return new MethodReferenceTarget(exprType, isClassRef);
}
return null;
}
/**
* Build a qualified name string from a MemberAccessNode chain.
* Example: a.b.c.ClassName -> "a.b.c.ClassName"
*/
private String buildQualifiedName(ExpressionNode node) {
if (node instanceof ExpressionNode.IdentifierNode) {
return ((ExpressionNode.IdentifierNode) node).getName();
}
if (node instanceof ExpressionNode.MemberAccessNode) {
ExpressionNode.MemberAccessNode ma = (ExpressionNode.MemberAccessNode) node;
String baseName = buildQualifiedName(ma.getTarget());
if (baseName != null) {
return baseName + "." + ma.getMemberName();
}
}
return null;
}
/**
* Resolve a method reference (target::methodName).
*/
private TypeInfo resolveMethodReference(MethodReferenceTarget target, String methodName,
MethodInfo sam, TypeInfo expectedType) {
TypeInfo targetType = target.isClassRef && target.type instanceof ClassTypeInfo
? ((ClassTypeInfo) target.type).getInstanceType()
: target.type;
if (targetType == null || !targetType.hasMethod(methodName)) {
return TypeInfo.fromClass(Object.class);
}
// Find best matching method using OverloadSelector
MethodInfo method = findBestMethodForReference(targetType, methodName, sam, target.isClassRef);
if (method == null) {
return TypeInfo.fromClass(Object.class);
}
// Validate signature compatibility
String error = validateMethodSignature(method, sam, target.isClassRef, targetType);
if (error != null) {
return TypeInfo.fromClass(Object.class);
}
return expectedType;
}
/**
* Find the best matching method for a method reference using OverloadSelector logic.
*/
private MethodInfo findBestMethodForReference(TypeInfo targetType, String methodName,
MethodInfo sam, boolean isClassRef) {
List<MethodInfo> allOverloads = targetType.getAllMethodOverloads(methodName);
if (allOverloads.isEmpty()) {
return null;
}
// Extract SAM parameter types for overload matching
TypeInfo[] samParamTypes = extractSamParamTypes(sam, isClassRef, targetType);
// Filter candidates by arity first, then use OverloadSelector
List<MethodInfo> candidates = new ArrayList<>();
int expectedArity = samParamTypes.length;
for (MethodInfo method : allOverloads) {
int methodArity = method.getParameters().size();
// Direct match: instance::method or ClassName::staticMethod
if (methodArity == expectedArity) {
candidates.add(method);
}
// Unbound instance method: ClassName::instanceMethod (first SAM param is receiver)
else if (isClassRef && methodArity == sam.getParameters().size() - 1 && !isStaticMethod(method)) {
candidates.add(method);
}
}
if (candidates.isEmpty()) {
return null;
}
if (candidates.size() == 1) {
return candidates.get(0);
}
// Use OverloadSelector for multi-candidate selection
return OverloadSelector.selectBestOverload(candidates, samParamTypes);
}
/**
* Extract the effective parameter types from SAM for method matching.
* For unbound instance methods, excludes the first receiver parameter.
*/
private TypeInfo[] extractSamParamTypes(MethodInfo sam, boolean isClassRef, TypeInfo targetType) {
List<FieldInfo> samParams = sam.getParameters();
// For class references, we might be matching an unbound instance method
// where the first SAM param is the receiver - handled in findBestMethodForReference
TypeInfo[] types = new TypeInfo[samParams.size()];
for (int i = 0; i < samParams.size(); i++) {
types[i] = samParams.get(i).getTypeInfo();
}
return types;
}
/**
* Check if a method is static (Java reflection).
*/
private boolean isStaticMethod(MethodInfo method) {
java.lang.reflect.Method javaMethod = method.getJavaMethod();
if (javaMethod != null) {
return java.lang.reflect.Modifier.isStatic(javaMethod.getModifiers());
}
// For script-defined methods, assume non-static unless marked
return method.isStatic();
}
/**
* Validate method signature compatibility with SAM.
*/
private String validateMethodSignature(MethodInfo method, MethodInfo sam,
boolean isClassRef, TypeInfo targetType) {
List<FieldInfo> methodParams = method.getParameters();
List<FieldInfo> samParams = sam.getParameters();
int methodArity = methodParams.size();
int samArity = samParams.size();
// Check for unbound instance method reference (ClassName::instanceMethod)
boolean isUnbound = isClassRef && methodArity == samArity - 1 && !isStaticMethod(method);
// Validate parameter count
if (methodArity != samArity && !isUnbound) {
return "Parameter count mismatch";
}
// Validate parameter types
int offset = isUnbound ? 1 : 0;
for (int i = 0; i < methodArity; i++) {
TypeInfo methodParamType = methodParams.get(i).getTypeInfo();
TypeInfo samParamType = samParams.get(i + offset).getTypeInfo();
if (methodParamType != null && samParamType != null) {
if (!TypeChecker.isTypeCompatible(methodParamType, samParamType)) {
return "Parameter type mismatch at position " + (i + 1);
}
}
}
// For unbound reference, validate receiver compatibility
if (isUnbound && samArity > 0) {
TypeInfo firstSamParam = samParams.get(0).getTypeInfo();
if (firstSamParam != null && !TypeChecker.isTypeCompatible(targetType, firstSamParam)) {
return "Receiver type mismatch";
}
}
// Validate return type (covariant)
TypeInfo methodReturn = method.getReturnType();
TypeInfo samReturn = sam.getReturnType();
if (samReturn != null && methodReturn != null) {
boolean samIsVoid = "void".equals(samReturn.getFullName()) || samReturn.getJavaClass() == void.class;
if (!samIsVoid && !TypeChecker.isTypeCompatible(samReturn, methodReturn)) {
return "Return type mismatch";
}
}
return null;
}
/**
* Resolve a constructor reference (ClassName::new or Type[]::new).
*/
private TypeInfo resolveConstructorReference(MethodReferenceTarget target, MethodInfo sam,
TypeInfo expectedType) {
TypeInfo targetType = target.type;
// Handle array constructor reference: Type[]::new
if (isArrayType(targetType)) {
return resolveArrayConstructorReference(targetType, sam, expectedType);
}
// Get the actual class type for constructor lookup
TypeInfo classType = target.type instanceof ClassTypeInfo
? ((ClassTypeInfo) target.type).getInstanceType()
: target.type;
if (classType == null) {
return TypeInfo.fromClass(Object.class);
}
// Cannot construct interfaces or abstract classes
if (classType.getKind() == TypeInfo.Kind.INTERFACE) {
return TypeInfo.fromClass(Object.class);
}
// Extract SAM parameter types for constructor matching
List<FieldInfo> samParams = sam.getParameters();
TypeInfo[] paramTypes = new TypeInfo[samParams.size()];
for (int i = 0; i < samParams.size(); i++) {
paramTypes[i] = samParams.get(i).getTypeInfo();
}
// Find matching constructor
MethodInfo constructor = classType.findConstructor(paramTypes);
if (constructor == null && samParams.size() > 0) {
// Try by arity if exact type match fails
constructor = classType.findConstructor(samParams.size());
}
if (constructor == null) {
return TypeInfo.fromClass(Object.class);
}
// Validate return type compatibility
TypeInfo samReturn = sam.getReturnType();
if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, classType)) {
return TypeInfo.fromClass(Object.class);
}
return expectedType;
}
/**
* Resolve an array constructor reference: Type[]::new
* Must match IntFunction<Type[]> or similar single-int-param functional interface.
*/
private TypeInfo resolveArrayConstructorReference(TypeInfo arrayType, MethodInfo sam,
TypeInfo expectedType) {
List<FieldInfo> samParams = sam.getParameters();
// Array constructor takes exactly one int parameter (the size)
if (samParams.size() != 1) {
return TypeInfo.fromClass(Object.class);
}
TypeInfo sizeParam = samParams.get(0).getTypeInfo();
if (sizeParam == null || !isIntLike(sizeParam)) {
return TypeInfo.fromClass(Object.class);
}
// Return type must be compatible with array type
TypeInfo samReturn = sam.getReturnType();
if (samReturn != null && !TypeChecker.isTypeCompatible(samReturn, arrayType)) {
return TypeInfo.fromClass(Object.class);
}
return expectedType;
}
/**
* Check if a type is an array type.
*/
private boolean isArrayType(TypeInfo type) {
if (type == null) return false;
String name = type.getFullName();
return name != null && name.endsWith("[]");
}
/**
* Check if a type is int-like (int, Integer, or numeric that can represent array size).
*/
private boolean isIntLike(TypeInfo type) {
if (type == null) return false;
String name = type.getFullName();
return "int".equals(name) || "java.lang.Integer".equals(name)
|| "long".equals(name) || "java.lang.Long".equals(name);
}
public static ExpressionNode.TypeResolverContext createBasicContext() {
return new ExpressionNode.TypeResolverContext() {
public TypeInfo resolveIdentifier(String name) {
if ("true".equals(name) || "false".equals(name)) return TypeInfo.fromPrimitive("boolean");
if ("null".equals(name)) return TypeInfo.unresolved("null", "<null>");
return null;
}
public TypeInfo resolveMemberAccess(TypeInfo targetType, String memberName) { return null; }
public TypeInfo resolveMethodCall(TypeInfo targetType, String methodName, TypeInfo[] argTypes) { return null; }
public TypeInfo resolveArrayAccess(TypeInfo arrayType) {
String typeName = arrayType.getFullName();
if (typeName.endsWith("[]")) {
// For primitive arrays, we need to map back to primitive TypeInfo
String elementType = typeName.substring(0, typeName.length() - 2);
switch (elementType) {
case "int": case "long": case "float": case "double":
case "byte": case "short": case "char": case "boolean":
return TypeInfo.fromPrimitive(elementType);
default:
return TypeInfo.unresolved(elementType, elementType);
}
}
return null;
}
public TypeInfo resolveTypeName(String typeName) {
// Handle primitives
switch (typeName) {
case "int": case "long": case "float": case "double":
case "byte": case "short": case "char": case "boolean": case "void":
return TypeInfo.fromPrimitive(typeName);
default:
return TypeInfo.unresolved(typeName, typeName);
}
}
};
}
}
@@ -0,0 +1,147 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
public enum OperatorType {
ADD("+", 11, Associativity.LEFT, Category.ARITHMETIC),
SUBTRACT("-", 11, Associativity.LEFT, Category.ARITHMETIC),
MULTIPLY("*", 12, Associativity.LEFT, Category.ARITHMETIC),
DIVIDE("/", 12, Associativity.LEFT, Category.ARITHMETIC),
MODULO("%", 12, Associativity.LEFT, Category.ARITHMETIC),
ASSIGN("=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
ADD_ASSIGN("+=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
SUBTRACT_ASSIGN("-=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
MULTIPLY_ASSIGN("*=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
DIVIDE_ASSIGN("/=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
MODULO_ASSIGN("%=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
AND_ASSIGN("&=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
OR_ASSIGN("|=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
XOR_ASSIGN("^=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
LEFT_SHIFT_ASSIGN("<<=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
RIGHT_SHIFT_ASSIGN(">>=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
UNSIGNED_RIGHT_SHIFT_ASSIGN(">>>=", 1, Associativity.RIGHT, Category.ASSIGNMENT),
TERNARY_QUESTION("?", 2, Associativity.RIGHT, Category.TERNARY),
TERNARY_COLON(":", 2, Associativity.RIGHT, Category.TERNARY),
LOGICAL_OR("||", 3, Associativity.LEFT, Category.LOGICAL),
LOGICAL_AND("&&", 4, Associativity.LEFT, Category.LOGICAL),
BITWISE_OR("|", 5, Associativity.LEFT, Category.BITWISE),
BITWISE_XOR("^", 6, Associativity.LEFT, Category.BITWISE),
BITWISE_AND("&", 7, Associativity.LEFT, Category.BITWISE),
EQUALS("==", 8, Associativity.LEFT, Category.RELATIONAL),
NOT_EQUALS("!=", 8, Associativity.LEFT, Category.RELATIONAL),
LESS_THAN("<", 9, Associativity.LEFT, Category.RELATIONAL),
GREATER_THAN(">", 9, Associativity.LEFT, Category.RELATIONAL),
LESS_THAN_OR_EQUAL("<=", 9, Associativity.LEFT, Category.RELATIONAL),
GREATER_THAN_OR_EQUAL(">=", 9, Associativity.LEFT, Category.RELATIONAL),
LEFT_SHIFT("<<", 10, Associativity.LEFT, Category.BITWISE),
RIGHT_SHIFT(">>", 10, Associativity.LEFT, Category.BITWISE),
UNSIGNED_RIGHT_SHIFT(">>>", 10, Associativity.LEFT, Category.BITWISE),
UNARY_PLUS("+", 14, Associativity.RIGHT, Category.UNARY),
UNARY_MINUS("-", 14, Associativity.RIGHT, Category.UNARY),
LOGICAL_NOT("!", 14, Associativity.RIGHT, Category.UNARY),
BITWISE_NOT("~", 14, Associativity.RIGHT, Category.UNARY),
PRE_INCREMENT("++", 14, Associativity.RIGHT, Category.UNARY),
PRE_DECREMENT("--", 14, Associativity.RIGHT, Category.UNARY),
POST_INCREMENT("++", 15, Associativity.LEFT, Category.UNARY_POSTFIX),
POST_DECREMENT("--", 15, Associativity.LEFT, Category.UNARY_POSTFIX),
MEMBER_ACCESS(".", 16, Associativity.LEFT, Category.ACCESS),
ARRAY_ACCESS("[]", 16, Associativity.LEFT, Category.ACCESS),
METHOD_CALL("()", 16, Associativity.LEFT, Category.ACCESS),
CAST("(type)", 14, Associativity.RIGHT, Category.CAST),
INSTANCEOF("instanceof", 9, Associativity.LEFT, Category.INSTANCEOF);
private final String symbol;
private final int precedence;
private final Associativity associativity;
private final Category category;
OperatorType(String symbol, int precedence, Associativity associativity, Category category) {
this.symbol = symbol;
this.precedence = precedence;
this.associativity = associativity;
this.category = category;
}
public String getSymbol() { return symbol; }
public int getPrecedence() { return precedence; }
public Associativity getAssociativity() { return associativity; }
public Category getCategory() { return category; }
public boolean isBinary() {
return category == Category.ARITHMETIC || category == Category.RELATIONAL ||
category == Category.LOGICAL || category == Category.BITWISE;
}
public boolean isUnary() {
return category == Category.UNARY || category == Category.UNARY_POSTFIX;
}
public boolean isAssignment() {
return category == Category.ASSIGNMENT;
}
public boolean isComparison() {
return category == Category.RELATIONAL;
}
public enum Associativity { LEFT, RIGHT }
public enum Category {
ARITHMETIC, UNARY, UNARY_POSTFIX, RELATIONAL, LOGICAL, BITWISE,
TERNARY, INSTANCEOF, ASSIGNMENT, CAST, ACCESS
}
public static OperatorType fromSymbol(String symbol) {
for (OperatorType op : values()) {
if (op.symbol.equals(symbol)) return op;
}
return null;
}
public static OperatorType fromBinarySymbol(String symbol) {
switch (symbol) {
case "+": return ADD;
case "-": return SUBTRACT;
case "*": return MULTIPLY;
case "/": return DIVIDE;
case "%": return MODULO;
case "==": return EQUALS;
case "!=": return NOT_EQUALS;
case "<": return LESS_THAN;
case ">": return GREATER_THAN;
case "<=": return LESS_THAN_OR_EQUAL;
case ">=": return GREATER_THAN_OR_EQUAL;
case "&&": return LOGICAL_AND;
case "||": return LOGICAL_OR;
case "&": return BITWISE_AND;
case "|": return BITWISE_OR;
case "^": return BITWISE_XOR;
case "<<": return LEFT_SHIFT;
case ">>": return RIGHT_SHIFT;
case ">>>": return UNSIGNED_RIGHT_SHIFT;
case "=": return ASSIGN;
case "+=": return ADD_ASSIGN;
case "-=": return SUBTRACT_ASSIGN;
case "*=": return MULTIPLY_ASSIGN;
case "/=": return DIVIDE_ASSIGN;
case "%=": return MODULO_ASSIGN;
case "&=": return AND_ASSIGN;
case "|=": return OR_ASSIGN;
case "^=": return XOR_ASSIGN;
case "<<=": return LEFT_SHIFT_ASSIGN;
case ">>=": return RIGHT_SHIFT_ASSIGN;
case ">>>=": return UNSIGNED_RIGHT_SHIFT_ASSIGN;
default: return null;
}
}
}
@@ -0,0 +1,349 @@
package noppes.npcs.client.gui.util.script.interpreter.expression;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
public class TypeRules {
private static boolean isJsNumber(TypeInfo type) {
if (type == null || !type.isResolved()) return false;
String name = type.getSimpleName();
String full = type.getFullName();
return "number".equals(name) || "number".equals(full);
}
private static boolean isNumericPrimitiveOrWrapper(TypeInfo type) {
if (type == null || type.getJavaClass() == null) return false;
Class<?> cls = type.getJavaClass();
return cls == byte.class || cls == Byte.class ||
cls == short.class || cls == Short.class ||
cls == int.class || cls == Integer.class ||
cls == long.class || cls == Long.class ||
cls == float.class || cls == Float.class ||
cls == double.class || cls == Double.class ||
cls == char.class || cls == Character.class;
}
private static boolean isAny(TypeInfo type) {
if (type == null) return false;
if (type == TypeInfo.ANY) return true;
String full = type.getFullName();
return "any".equals(full);
}
public static boolean isNumeric(TypeInfo type) {
if (type == null || !type.isResolved()) return false;
if (isJsNumber(type)) return true;
String name = type.getSimpleName();
return "int".equals(name) || "long".equals(name) || "float".equals(name) ||
"double".equals(name) || "byte".equals(name) || "short".equals(name) || "char".equals(name);
}
public static boolean isIntegral(TypeInfo type) {
if (type == null || !type.isResolved()) return false;
String name = type.getSimpleName();
return "int".equals(name) || "long".equals(name) || "byte".equals(name) ||
"short".equals(name) || "char".equals(name);
}
public static boolean isFloatingPoint(TypeInfo type) {
if (type == null || !type.isResolved()) return false;
if (isJsNumber(type)) return true;
String name = type.getSimpleName();
return "float".equals(name) || "double".equals(name);
}
public static boolean isBoolean(TypeInfo type) {
if (type == null || !type.isResolved()) return false;
return "boolean".equals(type.getSimpleName());
}
public static boolean isString(TypeInfo type) {
if (type == null || !type.isResolved()) return false;
String name = type.getSimpleName();
String fullName = type.getFullName();
return "String".equals(name) || "java.lang.String".equals(fullName);
}
public static TypeInfo binaryNumericPromotion(TypeInfo left, TypeInfo right) {
if (isJsNumber(left) || isJsNumber(right)) return TypeInfo.NUMBER;
if (left == null || right == null || !left.isResolved() || !right.isResolved()) {
return null;
}
String l = left.getSimpleName();
String r = right.getSimpleName();
if ("double".equals(l) || "double".equals(r)) return TypeInfo.fromPrimitive("double");
if ("float".equals(l) || "float".equals(r)) return TypeInfo.fromPrimitive("float");
if ("long".equals(l) || "long".equals(r)) return TypeInfo.fromPrimitive("long");
return TypeInfo.fromPrimitive("int");
}
public static TypeInfo unaryNumericPromotion(TypeInfo type) {
if (type == null || !type.isResolved()) return null;
if (isJsNumber(type)) return TypeInfo.NUMBER;
String name = type.getSimpleName();
if ("double".equals(name) || "float".equals(name) || "long".equals(name)) return type;
if ("byte".equals(name) || "short".equals(name) || "char".equals(name) || "int".equals(name)) {
return TypeInfo.fromPrimitive("int");
}
return null;
}
public static TypeInfo resolveBinaryOperatorType(OperatorType op, TypeInfo left, TypeInfo right) {
if (op == null) return null;
switch (op.getCategory()) {
case ARITHMETIC:
if (op == OperatorType.ADD && (isString(left) || isString(right))) {
return validateAgainstExpectedType(TypeInfo.string());
}
if (isAny(left) || isAny(right)) {
return validateAgainstExpectedType(TypeInfo.ANY);
}
if (isNumeric(left) && isNumeric(right)) {
TypeInfo promoted = binaryNumericPromotion(left, right);
return validateAgainstExpectedType(promoted);
}
return null;
case RELATIONAL:
if (op == OperatorType.EQUALS || op == OperatorType.NOT_EQUALS) {
return TypeInfo.fromPrimitive("boolean");
}
if (isAny(left) || isAny(right)) {
return TypeInfo.fromPrimitive("boolean");
}
if (isNumeric(left) && isNumeric(right)) {
return TypeInfo.fromPrimitive("boolean");
}
return null;
case LOGICAL:
if (isAny(left) || isAny(right)) {
return TypeInfo.ANY;
}
if (isBoolean(left) && isBoolean(right)) {
return TypeInfo.fromPrimitive("boolean");
}
return null;
case BITWISE:
if (isAny(left) || isAny(right)) {
return validateAgainstExpectedType(TypeInfo.ANY);
}
if (op == OperatorType.LEFT_SHIFT || op == OperatorType.RIGHT_SHIFT ||
op == OperatorType.UNSIGNED_RIGHT_SHIFT) {
if (isIntegral(left)) {
TypeInfo promoted = unaryNumericPromotion(left);
return validateAgainstExpectedType(promoted);
}
}
if (isIntegral(left) && isIntegral(right)) {
TypeInfo promoted = binaryNumericPromotion(left, right);
return validateAgainstExpectedType(promoted);
}
if (isBoolean(left) && isBoolean(right)) {
return TypeInfo.fromPrimitive("boolean");
}
return null;
case ASSIGNMENT:
return left;
default:
return null;
}
}
public static TypeInfo resolveUnaryOperatorType(OperatorType op, TypeInfo operand) {
if (op == null || operand == null || !operand.isResolved()) {
return null;
}
if (isAny(operand)) {
if (op == OperatorType.LOGICAL_NOT) {
return TypeInfo.fromPrimitive("boolean");
}
return TypeInfo.ANY;
}
switch (op) {
case UNARY_PLUS:
case UNARY_MINUS:
if (isNumeric(operand)) {
TypeInfo promoted = unaryNumericPromotion(operand);
return validateAgainstExpectedType(promoted);
}
return null;
case BITWISE_NOT:
if (isIntegral(operand)) {
TypeInfo promoted = unaryNumericPromotion(operand);
return validateAgainstExpectedType(promoted);
}
return null;
case LOGICAL_NOT:
if (isBoolean(operand)) return TypeInfo.fromPrimitive("boolean");
return null;
case PRE_INCREMENT:
case PRE_DECREMENT:
case POST_INCREMENT:
case POST_DECREMENT:
if (isNumeric(operand)) return operand;
return null;
default:
return null;
}
}
/**
* Check if sourceType can be assigned to targetType.
* Handles primitives, numeric conversions, null compatibility, and reference types.
*/
public static boolean isAssignmentCompatible(TypeInfo sourceType, TypeInfo targetType) {
if (sourceType == null || targetType == null)
return false;
if (!targetType.isResolved())
return true; // Can't validate against unresolved type
if (isAny(sourceType) || isAny(targetType)) {
return true;
}
// Null literal can be assigned to any reference type
if ("<null>".equals(sourceType.getFullName())) {
return !isPrimitive(targetType);
}
if (!sourceType.isResolved())
return false;
if ((isJsNumber(sourceType) && isNumericPrimitiveOrWrapper(targetType)) ||
(isJsNumber(targetType) && isNumericPrimitiveOrWrapper(sourceType))) {
return true;
}
// Exact match
if (sourceType.equals(targetType))
return true;
if (sourceType.getFullName().equals(targetType.getFullName()))
return true;
// Numeric conversions (widening primitive conversions)
String source = sourceType.getSimpleName();
String target = targetType.getSimpleName();
// byte -> short, int, long, float, double
if ("byte".equals(source)) {
return "short".equals(target) || "int".equals(target) || "long".equals(target) ||
"float".equals(target) || "double".equals(target);
}
// short -> int, long, float, double
if ("short".equals(source)) {
return "int".equals(target) || "long".equals(target) ||
"float".equals(target) || "double".equals(target);
}
// char -> int, long, float, double
if ("char".equals(source)) {
return "int".equals(target) || "long".equals(target) ||
"float".equals(target) || "double".equals(target);
}
// int -> long, float, double
if ("int".equals(source)) {
return "long".equals(target) || "float".equals(target) || "double".equals(target);
}
// long -> float, double
if ("long".equals(source)) {
return "float".equals(target) || "double".equals(target);
}
// float -> double
if ("float".equals(source)) {
return "double".equals(target);
}
// For reference types, we'd need inheritance/interface checking
// For now, return false for incompatible types
return false;
}
/**
* Check if a type is a primitive type.
*/
public static boolean isPrimitive(TypeInfo type) {
if (type == null || !type.isResolved())
return false;
String name = type.getSimpleName();
return "int".equals(name) || "long".equals(name) || "float".equals(name) ||
"double".equals(name) || "byte".equals(name) || "short".equals(name) ||
"char".equals(name) || "boolean".equals(name) || "void".equals(name);
}
/**
* Validate a computed type against the current expected type context.
* If there's an expected type and the computed type is compatible, returns the expected type.
* If incompatible, returns the computed type so the error can be properly reported.
* If no expected type context, returns the computed type unchanged.
*
* @param computedType The type computed by normal type rules
* @return The type to use (either expectedType if compatible, or computedType)
*/
public static TypeInfo validateAgainstExpectedType(TypeInfo computedType) {
if (computedType == null) return null;
TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE;
if (expectedType != null && expectedType.isResolved() && computedType.isResolved()) {
if (isAssignmentCompatible(computedType, expectedType)) {
return expectedType;
}
// Return computed type so error can be detected (incompatible with expected)
return computedType;
}
return computedType;
}
public static TypeInfo resolveTernaryType(TypeInfo thenType, TypeInfo elseType) {
if (thenType == null && elseType == null) return null;
if (thenType == null) return elseType;
if (elseType == null) return thenType;
// Handle null literal type
boolean thenIsNull = "<null>".equals(thenType.getFullName());
boolean elseIsNull = "<null>".equals(elseType.getFullName());
if (thenIsNull && elseIsNull) return thenType; // both null -> null
if (thenIsNull) return elseType; // null : T -> T
if (elseIsNull) return thenType; // T : null -> T
if (!thenType.isResolved()) return elseType.isResolved() ? elseType : null;
if (!elseType.isResolved()) return thenType;
// If there's an expected type context, validate both branches against it
TypeInfo expectedType = ExpressionTypeResolver.CURRENT_EXPECTED_TYPE;
if (expectedType != null && expectedType.isResolved()) {
boolean thenCompatible = isAssignmentCompatible(thenType, expectedType);
boolean elseCompatible = isAssignmentCompatible(elseType, expectedType);
// If both branches are compatible with expected type, return expected type
if (thenCompatible && elseCompatible) {
return expectedType;
} else if (!elseCompatible) {
// Return the incompatible type so error can be detected
return elseType;
} else {
// thenType is incompatible
return thenType;
}
}
// Normal ternary type resolution without expected type context
if (thenType.equals(elseType)) return thenType;
if (isNumeric(thenType) && isNumeric(elseType)) {
return binaryNumericPromotion(thenType, elseType);
}
return thenType;
}
}
@@ -0,0 +1,249 @@
package noppes.npcs.client.gui.util.script.interpreter.field;
import noppes.npcs.client.gui.util.script.ScopeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
/**
* Metadata for assignment validation.
* Tracks assignment statements like "varName = expr" or "obj.field = expr"
* and validates type compatibility, access modifiers, and final status.
*
* Each AssignmentInfo represents a single assignment to a field.
* The positions are:
* - lhsStart/lhsEnd: The target variable/field position (left-hand side)
* - rhsStart/rhsEnd: From first token of RHS to the semicolon (inclusive)
*/
public class AssignmentInfo {
/**
* Error type for assignment validation.
*/
public enum ErrorType {
NONE,
TYPE_MISMATCH, // Assigned value type doesn't match target type
FINAL_REASSIGNMENT, // Attempting to reassign a final field
PRIVATE_ACCESS, // Attempting to access private field
PROTECTED_ACCESS, // Attempting to access protected field from invalid context
UNRESOLVED_TARGET, // Target variable/field doesn't exist
STATIC_CONTEXT_ERROR, // Accessing instance field from static context
DUPLICATE_DECLARATION // Variable is already defined in the scope
}
// Statement position
private final int statementStart; // Absolute start of statement (includes modifiers/type for declarations)
// Target/LHS info
private final String targetName;
private final int lhsStart; // Position of first char of target variable/field
private final int lhsEnd; // Position after last char of target
private final TypeInfo targetType; // Declared type of target
// Source/RHS info (from first token of RHS expression to semicolon)
private final int rhsStart; // Position of first non-whitespace char after '='
private final int rhsEnd; // Position of ';' (inclusive in the range for underlining)
private final TypeInfo sourceType; // Resolved type of RHS expression
private final String sourceExpr; // The RHS expression text (for display)
// For chained field access (e.g., obj.field = value)
private final TypeInfo receiverType; // Type of receiver (null for simple variables)
private final Field reflectionField; // Java reflection field (for modifier checks)
// Flag for script-defined final variables
private final boolean isFinal;
// Validation
private ErrorType errorType = ErrorType.NONE;
private String errorMessage;
private String requiredType;
private String providedType;
private ScopeInfo scopeInfo;
public AssignmentInfo(String targetName, int statementStart, int lhsStart, int lhsEnd,
TypeInfo targetType, int rhsStart, int rhsEnd,
TypeInfo sourceType, String sourceExpr,
TypeInfo receiverType, java.lang.reflect.Field reflectionField,
boolean isFinal) {
this.targetName = targetName;
this.statementStart = statementStart;
this.lhsStart = lhsStart;
this.lhsEnd = lhsEnd;
this.targetType = targetType;
this.rhsStart = rhsStart;
this.rhsEnd = rhsEnd;
this.sourceType = sourceType;
this.sourceExpr = sourceExpr;
this.receiverType = receiverType;
this.reflectionField = reflectionField;
this.isFinal = isFinal;
}
/**
* Factory method to create an AssignmentInfo representing a duplicate declaration error.
* Only the LHS (variable name) position is relevant for underlining.
*/
public static AssignmentInfo duplicateDeclaration(String varName, int nameStart, int nameEnd, String errorMessage) {
AssignmentInfo info = new AssignmentInfo(
varName,
nameStart, // statementStart = nameStart for underline positioning
nameStart, // lhsStart
nameEnd, // lhsEnd
null, // targetType
-1, // rhsStart (not applicable)
-1, // rhsEnd (not applicable)
null, // sourceType
null, // sourceExpr
null, // receiverType
null, // reflectionField
false // isFinal
);
info.setError(ErrorType.DUPLICATE_DECLARATION, errorMessage);
return info;
}
// ==================== VALIDATION ====================
/**
* Validate this assignment.
* Checks type compatibility, final status, and access modifiers.
*/
public void validate() {
// Check for final field reassignment from reflection
if (reflectionField != null && Modifier.isFinal(reflectionField.getModifiers())) {
setError(ErrorType.FINAL_REASSIGNMENT,
"Cannot assign a value to final variable '" + targetName + "'");
return;
}
// Check for final field reassignment from FieldInfo (script-defined finals)
if (isFinal) {
setError(ErrorType.FINAL_REASSIGNMENT,
"Cannot assign a value to final variable '" + targetName + "'");
return;
}
// Check access modifiers for chained field access
if (reflectionField != null && receiverType != null) {
int mods = reflectionField.getModifiers();
if (Modifier.isPrivate(mods)) {
setError(ErrorType.PRIVATE_ACCESS,
"'" + targetName + "' has private access in '" + receiverType.getFullName() + "'");
return;
}
}
// Check type compatibility
if (targetType != null && sourceType != null) {
if (!TypeChecker.isTypeCompatible(targetType, sourceType)) {
this.requiredType = targetType.getSimpleName();
this.providedType = sourceType.getSimpleName();
setError(ErrorType.TYPE_MISMATCH, buildTypeMismatchMessage());
}
}
}
/**
* Build a formatted type mismatch error message (IntelliJ style).
*/
private String buildTypeMismatchMessage() {
return "Provided type: " + providedType + "\nRequired: " + requiredType;
}
private void setError(ErrorType type, String message) {
this.errorType = type;
this.errorMessage = message;
}
// ==================== POSITION CHECKS ====================
/**
* Check if the given global position falls within the LHS (target) of this assignment.
*/
public boolean containsLhsPosition(int position) {
return position >= statementStart && position < lhsEnd;
}
/**
* Check if the given global position falls within the RHS (source) of this assignment.
*/
public boolean containsRhsPosition(int position) {
return position >= rhsStart && position <= rhsEnd;
}
/**
* Check if the given global position falls anywhere in this assignment (LHS or RHS).
*/
public boolean containsPosition(int position) {
return position >= statementStart && position <= rhsEnd;
}
// ==================== ERROR TYPE CHECKS ====================
/**
* Check if this is an LHS error (final reassignment, access errors, duplicate declarations).
* These errors should underline the LHS.
*/
public boolean isLhsError() {
return errorType == ErrorType.FINAL_REASSIGNMENT ||
errorType == ErrorType.PRIVATE_ACCESS ||
errorType == ErrorType.PROTECTED_ACCESS ||
errorType == ErrorType.STATIC_CONTEXT_ERROR ||
errorType == ErrorType.DUPLICATE_DECLARATION;
}
/**
* Check if this is an RHS error (type mismatch).
* These errors should underline the RHS.
*/
public boolean isRhsError() {
return false;
}
public boolean isFullLineError() {
return errorType == ErrorType.TYPE_MISMATCH;
}
// ==================== GETTERS ====================
public String getTargetName() { return targetName; }
public int getStatementStart() { return statementStart; }
public int getLhsStart() { return lhsStart; }
public int getLhsEnd() { return lhsEnd; }
public TypeInfo getTargetType() { return targetType; }
public int getRhsStart() { return rhsStart; }
public int getRhsEnd() { return rhsEnd; }
public TypeInfo getSourceType() { return sourceType; }
public String getSourceExpr() { return sourceExpr; }
public TypeInfo getReceiverType() { return receiverType; }
public java.lang.reflect.Field getReflectionField() { return reflectionField; }
public ErrorType getErrorType() { return errorType; }
public String getErrorMessage() { return errorMessage; }
public String getRequiredType() { return requiredType; }
public String getProvidedType() { return providedType; }
public boolean hasError() { return errorType != ErrorType.NONE; }
public boolean hasTypeMismatch() { return errorType == ErrorType.TYPE_MISMATCH; }
public boolean hasFinalReassignment() { return errorType == ErrorType.FINAL_REASSIGNMENT; }
public boolean hasAccessError() { return errorType == ErrorType.PRIVATE_ACCESS || errorType == ErrorType.PROTECTED_ACCESS; }
public ScopeInfo getScopeInfo() { return scopeInfo; }
public void setScopeInfo(ScopeInfo scopeInfo) { this.scopeInfo = scopeInfo; }
@Override
public String toString() {
return "AssignmentInfo{" +
"target='" + targetName + "', " +
"stmt=" + statementStart + ", " +
"lhs=[" + lhsStart + "-" + lhsEnd + "], " +
"rhs=[" + rhsStart + "-" + rhsEnd + "], " +
"targetType=" + targetType + ", " +
"sourceType=" + sourceType + ", " +
"error=" + errorType +
'}';
}
}
@@ -0,0 +1,317 @@
package noppes.npcs.client.gui.util.script.interpreter.field;
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.ScriptTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Represents an enum constant declaration with its constructor call.
* Combines a FieldInfo (the constant itself) with a MethodCallInfo (the constructor call validation).
* This allows us to reuse existing method call validation logic for enum constructor matching.
*/
public class EnumConstantInfo {
private final FieldInfo fieldInfo;
private final MethodCallInfo constructorCall; // Can be null if no args provided
private final TypeInfo enumType;
public EnumConstantInfo(FieldInfo fieldInfo, MethodCallInfo constructorCall, TypeInfo enumType) {
this.fieldInfo = fieldInfo;
this.constructorCall = constructorCall;
this.enumType = enumType;
}
/**
* Parse enum constants from an enum body.
* Returns a list of EnumConstantInfo objects, each representing one constant.
*
* @param enumType The enum type being parsed
* @param bodyText The text content of the enum body
* @param bodyOffset The absolute offset where the enum body starts
* @param keywordPattern Pattern to detect Java keywords (to skip them)
* @return List of parsed enum constants
*/
public static List<EnumConstantInfo> parseEnumConstants(
ScriptTypeInfo enumType,
String bodyText,
int bodyOffset,
Pattern keywordPattern) {
List<EnumConstantInfo> constants = new ArrayList<>();
// Find where enum constants end (first semicolon not in parens, or first method/field declaration)
int constantsEnd = findEnumConstantsEnd(bodyText);
if (constantsEnd <= 0) {
// No semicolon found - entire body might be constants
constantsEnd = bodyText.length();
}
// Pattern to match enum constants: NAME or NAME() or NAME(args)
Pattern constantPattern = Pattern.compile(
"([A-Za-z_][a-zA-Z0-9_]*)\\s*(\\(([^)]*)\\))?");
Matcher m = constantPattern.matcher(bodyText);
int lastEnd = 0;
while (m.find()) {
// Skip if we're past the constants section
if (m.start() >= constantsEnd) {
break;
}
int absPos = bodyOffset + m.start();
if (ScriptDocument.INSTANCE.isExcluded(absPos)) {
lastEnd = m.end();
continue;
}
// Check if this is at a valid position (after comma or at start)
String beforeMatch = bodyText.substring(lastEnd, m.start()).trim();
if (!beforeMatch.isEmpty() && !beforeMatch.equals(",")) {
// Not a valid enum constant position - might be inside parens
continue;
}
String constantName = m.group(1);
String argsClause = m.group(2); // includes parens if present
String args = m.group(3); // just the args without parens
// Skip keywords
if (keywordPattern.matcher(constantName).matches()) {
lastEnd = m.end();
continue;
}
// Determine init range (the constructor arguments)
int initStart = -1;
int initEnd = -1;
// Create MethodCallInfo for constructor validation if args present
MethodCallInfo constructorCall = null;
if (argsClause != null && !argsClause.isEmpty()) {
initStart = bodyOffset + m.start(2); // Position of '('
initEnd = bodyOffset + m.end(2) - 1; // Position after ')'
constructorCall = createConstructorCall(
enumType,
constantName,
absPos,
initStart,
initEnd
);
} else if (enumType.hasConstructors()) {
// No args provided, but enum has constructors - validate against no-arg constructor
initStart = absPos + constantName.length();
initEnd = initStart;
constructorCall = createConstructorCall(
enumType,
constantName,
absPos,
initStart,
initEnd
);
}
// Create FieldInfo for the enum constant
FieldInfo fieldInfo = FieldInfo.enumConstant(
constantName,
enumType,
absPos,
initStart,
initEnd,
null
);
EnumConstantInfo constantInfo = new EnumConstantInfo(fieldInfo, constructorCall, enumType);
fieldInfo.setEnumConstantInfo(constantInfo);
constants.add(constantInfo);
lastEnd = m.end();
}
return constants;
}
/**
* Create a MethodCallInfo for an enum constant's constructor call.
* Validates the arguments against available constructors.
*/
private static MethodCallInfo createConstructorCall(
ScriptTypeInfo enumType,
String constantName,
int constantStart,
int openParenPos,
int closeParenPos) {
List<MethodInfo> constructors = enumType.getConstructors();
// Parse arguments - pass absolute positions (bodyOffset accounts for the offset into the document)
List<MethodCallInfo.Argument> arguments = ScriptDocument.INSTANCE.parseMethodArguments(
openParenPos + 1,
closeParenPos,
null
);
// Find matching constructor
MethodInfo matchedConstructor = null;
List<MethodInfo> candidates = new ArrayList<>();
for (MethodInfo constructor : constructors) {
candidates.add(constructor);
if (constructor.getParameterCount() == arguments.size()) {
matchedConstructor = constructor;
break;
}
}
// Create MethodCallInfo
MethodCallInfo callInfo = new MethodCallInfo(
constantName, // Use constant name as "method" name
constantStart,
openParenPos,
openParenPos,
closeParenPos,
arguments,
enumType, // Receiver is the enum type
matchedConstructor
);
callInfo.setConstructor(true);
callInfo.validate();
return callInfo;
}
/**
* Create EnumConstantInfo from a reflection-based enum constant.
* Used for enum constants from external libraries (via reflection).
*
* @param constantName The name of the enum constant
* @param enumType The enum type this constant belongs to
* @return EnumConstantInfo representing this constant, or null if not found
*/
public static EnumConstantInfo fromReflection(String constantName, TypeInfo enumType, Field javaField) {
if (enumType == null || enumType.getJavaClass() == null || !enumType.getJavaClass().isEnum()) {
return null;
}
try {
FieldInfo fieldInfo = FieldInfo.enumConstant(
constantName,
enumType,
-1, // No declaration offset for reflection-based constants
-1,
-1,
javaField
);
EnumConstantInfo enumInfo = new EnumConstantInfo(fieldInfo, null, enumType);
fieldInfo.setEnumConstantInfo(enumInfo);
return enumInfo;
} catch (SecurityException e) {
// Constant not found or access denied
return null;
}
}
/**
* Find where enum constants section ends.
* Returns the position of the first semicolon that's not inside parentheses,
* or the position of the first method/field declaration.
*/
private static int findEnumConstantsEnd(String bodyText) {
int parenDepth = 0;
boolean foundConstant = false;
for (int i = 0; i < bodyText.length(); i++) {
char c = bodyText.charAt(i);
if (c == '(') {
parenDepth++;
} else if (c == ')') {
parenDepth--;
} else if (c == ';' && parenDepth == 0) {
return i;
} else if (c == '{' && parenDepth == 0) {
// Start of method body - constants end before this
return findStatementStart(bodyText, i);
}
// Track if we've found at least one identifier (potential constant)
if (Character.isJavaIdentifierStart(c)) {
foundConstant = true;
}
}
// If we found constants but no semicolon, return the full length
return foundConstant ? bodyText.length() : -1;
}
/**
* Find the start of a statement by going backwards from a position.
*/
private static int findStatementStart(String text, int fromPos) {
int pos = fromPos - 1;
int parenDepth = 0;
while (pos >= 0) {
char c = text.charAt(pos);
if (c == ')')
parenDepth++;
else if (c == '(')
parenDepth--;
else if ((c == ',' || c == ';' || c == '{' || c == '}') && parenDepth == 0) {
return pos + 1;
}
pos--;
}
return 0;
}
// ==================== GETTERS ====================
public FieldInfo getFieldInfo() {
return fieldInfo;
}
public MethodCallInfo getConstructorCall() {
return constructorCall;
}
public TypeInfo getEnumType() {
return enumType;
}
public String getName() {
return fieldInfo.getName();
}
public int getDeclarationOffset() {
return fieldInfo.getDeclarationOffset();
}
/**
* Check if this enum constant has any errors (constructor mismatch, etc).
*/
public boolean hasError() {
return (constructorCall != null && constructorCall.hasError());
}
/**
* Get the error message if any.
*/
public String getErrorMessage() {
return null;
}
}
@@ -0,0 +1,152 @@
package noppes.npcs.client.gui.util.script.interpreter.field;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
/**
* Metadata for field access validation.
* Parallel to MethodCallInfo but for field accesses like `obj.field` or `Class.staticField`.
*/
public class FieldAccessInfo {
/**
* Validation error type for field accesses.
*/
public enum ErrorType {
NONE,
TYPE_MISMATCH, // Field type doesn't match expected type (e.g., assignment LHS)
UNRESOLVED_FIELD, // Field doesn't exist on the receiver type
STATIC_ACCESS_ERROR // Trying to access instance field statically or vice versa
}
private final String fieldName;
private final int fieldNameStart;
private final int fieldNameEnd;
private final TypeInfo receiverType; // The type on which this field is accessed
private final FieldInfo resolvedField; // The resolved field (null if unresolved)
private final boolean isStaticAccess; // True if this is Class.field style access
private TypeInfo expectedType; // Expected field type (from assignment LHS, etc.)
private ErrorType errorType = ErrorType.NONE;
private String errorMessage;
public FieldAccessInfo(String fieldName, int fieldNameStart, int fieldNameEnd,
TypeInfo receiverType, FieldInfo resolvedField, boolean isStaticAccess) {
this.fieldName = fieldName;
this.fieldNameStart = fieldNameStart;
this.fieldNameEnd = fieldNameEnd;
this.receiverType = receiverType;
this.resolvedField = resolvedField;
this.isStaticAccess = isStaticAccess;
}
// Getters
public String getFieldName() {
return fieldName;
}
public int getFieldNameStart() {
return fieldNameStart;
}
public int getFieldNameEnd() {
return fieldNameEnd;
}
public TypeInfo getReceiverType() {
return receiverType;
}
public FieldInfo getResolvedField() {
return resolvedField;
}
public boolean isStaticAccess() {
return isStaticAccess;
}
public boolean isEnumConstantAccess() {
return resolvedField != null && resolvedField.isEnumConstant();
}
public TypeInfo getExpectedType() {
return expectedType;
}
public void setExpectedType(TypeInfo expectedType) {
this.expectedType = expectedType;
}
public ErrorType getErrorType() {
return errorType;
}
public String getErrorMessage() {
return errorMessage;
}
/**
* Validate this field access.
* Checks type compatibility with expected type.
*/
public void validate() {
// Check if field was resolved
if (resolvedField == null) {
setError(ErrorType.UNRESOLVED_FIELD, "Cannot resolve field '" + fieldName + "'");
return;
}
// Check return type compatibility with expected type (e.g., assignment LHS)
if (expectedType != null && resolvedField != null) {
TypeInfo fieldType = resolvedField.getTypeInfo();
if (fieldType != null && !TypeChecker.isTypeCompatible(expectedType, fieldType)) {
//extra space is necessary for alignment
//setError(ErrorType.TYPE_MISMATCH, "Provided type: " + fieldType.getSimpleName()+
//"\nRequired: " + expectedType.getSimpleName());
}
}
}
private void setError(ErrorType type, String message) {
this.errorType = type;
this.errorMessage = message;
}
/**
* Check if this field access has any validation error.
*/
public boolean hasError() {
return errorType != ErrorType.NONE;
}
/**
* Check if this is a type mismatch error.
*/
public boolean hasTypeMismatch() {
return errorType == ErrorType.TYPE_MISMATCH;
}
/**
* Check if this is an unresolved field error.
*/
public boolean hasUnresolvedField() {
return errorType == ErrorType.UNRESOLVED_FIELD;
}
/**
* Check if this is a static access error.
*/
public boolean hasStaticAccessError() {
return errorType == ErrorType.STATIC_ACCESS_ERROR;
}
@Override
public String toString() {
return "FieldAccessInfo{" +
"fieldName='" + fieldName + "', " +
"receiverType=" + receiverType + ", " +
"resolvedField=" + resolvedField + ", " +
"errorType=" + errorType +
'}';
}
}
@@ -0,0 +1,616 @@
package noppes.npcs.client.gui.util.script.interpreter.field;
import noppes.npcs.client.gui.util.script.ScopeInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSFieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
import noppes.npcs.client.gui.util.script.interpreter.bridge.DtsJavaBridge;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Metadata for a field (variable) declaration or reference.
* Tracks the field name, type, scope, and declaration location.
* Also tracks all assignments made to this field throughout the script.
*/
public final class FieldInfo {
public enum Scope {
GLOBAL, // Class-level field
LOCAL, // Local variable inside a method
PARAMETER, // Method parameter
ENUM_CONSTANT // Enum constant value
}
private final String name;
private final Scope scope;
private final TypeInfo declaredType; // The declared type (e.g., String, List<Integer>)
private final int declarationOffset; // Where this field was declared in the source
private final boolean resolved;
private final String documentation; // Javadoc/comment documentation for this field
private JSDocInfo jsDocInfo; // Parsed JSDoc info for this method (may be null)
// Initialization value range (for displaying "= value" in hover info)
private final int initStart; // Position of '=' or -1 if no initializer
private final int initEnd; // Position after initializer (before ';') or -1
// For local/parameter fields, track the containing method
private final MethodInfo containingMethod;
// Modifiers for script-defined fields
private final int modifiers; // Java Modifier flags (e.g., Modifier.FINAL)
// Reflection field for external types
private final Field reflectionField;
private ScopeInfo scopeInfo;
// Assignments to this field (populated after parsing)
private final List<AssignmentInfo> assignments = new ArrayList<>();
// Declaration assignment (for initial value validation)
private AssignmentInfo declarationAssignment;
// Enum constant specific info
private EnumConstantInfo enumConstantInfo;
private boolean isVarArg = false;
// Type inference support (for JavaScript "any" typed variables)
// When a variable is declared without type (var x;), it starts as "any" but can be
// refined through assignments or JSDoc comments
private TypeInfo inferredType;
private FieldInfo(String name, Scope scope, TypeInfo declaredType,
int declarationOffset, boolean resolved, MethodInfo containingMethod,
String documentation, int initStart, int initEnd, int modifiers,
Field reflectionField) {
this.name = name;
this.scope = scope;
this.declaredType = declaredType;
this.declarationOffset = declarationOffset;
this.resolved = resolved;
this.containingMethod = containingMethod;
this.documentation = documentation;
this.initStart = initStart;
this.initEnd = initEnd;
this.modifiers = modifiers;
this.reflectionField = reflectionField;
}
// Factory methods
public static FieldInfo globalField(String name, TypeInfo type, int declOffset) {
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null, null, -1,
-1, 0, null);
}
public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation) {
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null,
documentation, -1, -1, 0, null);
}
public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart, int initEnd) {
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null,
documentation, initStart, initEnd, 0, null);
}
public static FieldInfo globalField(String name, TypeInfo type, int declOffset, String documentation, int initStart,
int initEnd, int modifiers) {
return new FieldInfo(name, Scope.GLOBAL, type, declOffset, type != null && type.isResolved(), null,
documentation, initStart, initEnd, modifiers, null);
}
public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method) {
return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null, -1,
-1, 0, null);
}
public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart, int initEnd) {
return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null,
initStart, initEnd, 0, null);
}
public static FieldInfo localField(String name, TypeInfo type, int declOffset, MethodInfo method, int initStart,
int initEnd, int modifiers) {
return new FieldInfo(name, Scope.LOCAL, type, declOffset, type != null && type.isResolved(), method, null,
initStart, initEnd, modifiers, null);
}
public static FieldInfo parameter(String name, TypeInfo type, int declOffset, MethodInfo method) {
return new FieldInfo(name, Scope.PARAMETER, type, declOffset, type != null && type.isResolved(), method, null,
-1, -1, 0, null);
}
public static FieldInfo unresolved(String name, Scope scope) {
return new FieldInfo(name, scope, null, -1, false, null, null, -1, -1, 0, null);
}
/**
* Create a FieldInfo from reflection data for method parameters.
*/
public static FieldInfo reflectionParam(String name, TypeInfo type) {
return new FieldInfo(name, Scope.PARAMETER, type, -1, true, null, null, -1, -1, 0, null);
}
/**
* Create a FieldInfo for a synthetic/external field.
* Used for built-in types like Nashorn's Java object.
*/
public static FieldInfo external(String name, TypeInfo type, String documentation, int modifiers) {
return new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null);
}
/**
* Create a FieldInfo from reflection data for a class field.
* Preserves generic type information like List<String>.
*/
public static FieldInfo fromReflection(Field field, TypeInfo containingType) {
String name = field.getName();
// Use getGenericType() to preserve generic information
TypeInfo type = TypeInfo.fromGenericType(field.getGenericType());
if (type == null) {
type = TypeInfo.fromClass(field.getType());
}
// If the receiver is parameterized, substitute class type variables in the field type
if (GenericContext.hasGenerics(containingType)) {
Map<String, TypeInfo> receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType);
if (!receiverBindings.isEmpty()) {
type = TypeSubstitutor.substitute(type, receiverBindings);
}
}
// Check if this is an enum constant
if (field.isEnumConstant()) {
// Create enum constant FieldInfo
EnumConstantInfo constantInfo = EnumConstantInfo.fromReflection(name, containingType,field);
if (constantInfo != null)
return constantInfo.getFieldInfo();
}
// Try to find matching JSFieldInfo to bridge over documentation
JSFieldInfo jsField = DtsJavaBridge.findMatchingField(field, containingType);
String documentation = null;
JSDocInfo jsDocInfo = null;
if (jsField != null) {
jsDocInfo = jsField.getJsDocInfo();
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
documentation = jsDocDesc != null ? jsDocDesc : jsField.getDocumentation();
}
FieldInfo fieldInfo = new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, field.getModifiers(), field);
if (jsDocInfo != null) {
fieldInfo.setJSDocInfo(jsDocInfo);
}
return fieldInfo;
}
/**
* Create a FieldInfo from a JSFieldInfo (parsed from .d.ts files).
* Used when resolving field access on JavaScript types.
*
* @param jsField The JavaScript field info from the type registry
* @param containingType The TypeInfo that owns this field
* @return A FieldInfo representing the JavaScript field
*/
public static FieldInfo fromJSField(JSFieldInfo jsField, TypeInfo containingType) {
String name = jsField.getName();
TypeInfo type = jsField.getResolvedType(containingType);
if (type == null || !type.isResolved()) {
type = resolveJSType(jsField.getType());
}
// JS fields are public by default, readonly maps to final
int modifiers = Modifier.PUBLIC;
if (jsField.isReadonly()) {
modifiers |= Modifier.FINAL;
}
if (jsField.isStatic()) {
modifiers |= Modifier.STATIC;
}
// Use documentation if available
JSDocInfo jsDocInfo = jsField.getJsDocInfo();
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
String documentation = jsDocDesc != null ? jsDocDesc : jsField.getDocumentation();
FieldInfo fieldInfo = new FieldInfo(name, Scope.GLOBAL, type, -1, true, null, documentation, -1, -1, modifiers, null);
fieldInfo.setJSDocInfo(jsDocInfo);
return fieldInfo;
}
/**
* Resolves a JavaScript type name to a TypeInfo.
* Handles primitives, mapped types, and custom types from the registry.
*/
private static TypeInfo resolveJSType(String jsTypeName) {
if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) {
return TypeInfo.fromPrimitive("void");
}
// Handle JS primitives
switch (jsTypeName) {
case "string":
return TypeInfo.fromClass(String.class);
case "number":
return TypeInfo.fromClass(double.class);
case "boolean":
return TypeInfo.fromClass(boolean.class);
case "any":
return TypeInfo.fromClass(Object.class);
case "void":
return TypeInfo.fromPrimitive("void");
}
// Handle array types
if (jsTypeName.endsWith("[]")) {
String elementType = jsTypeName.substring(0, jsTypeName.length() - 2);
TypeInfo elementTypeInfo = resolveJSType(elementType);
return TypeInfo.arrayOf(elementTypeInfo);
}
// Try to resolve from the JS type registry
JSTypeRegistry registry = JSTypeRegistry.getInstance();
if (registry != null) {
JSTypeInfo jsTypeInfo = registry.getType(jsTypeName);
if (jsTypeInfo != null) {
return TypeInfo.fromJSTypeInfo(jsTypeInfo);
}
}
// Fallback: unresolved type
return TypeInfo.unresolved(jsTypeName, jsTypeName);
}
/**
* Create a FieldInfo for an enum constant.
* @param name The constant name (e.g., "NORTH")
* @param type The enum type itself
* @param declOffset The declaration position
* @param initStart The position of '(' if args present, else -1
* @param initEnd The position after ')' if args present, else -1
*/
public static FieldInfo enumConstant(String name, TypeInfo type, int declOffset, int initStart, int initEnd,Field javaField) {
// Enum constants are implicitly public static final
int modifiers = Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL;
return new FieldInfo(name, Scope.ENUM_CONSTANT, type, declOffset, true, null, null, initStart, initEnd,
modifiers, javaField);
}
// ==================== ENUM HANDLING ====================
public void setEnumConstantInfo(EnumConstantInfo enumConstantInfo) {
this.enumConstantInfo = enumConstantInfo;
}
public EnumConstantInfo getEnumInfo() {
return enumConstantInfo;
}
/**
* Check if this is an enum constant.
*/
public boolean isEnumConstant() {
return scope == Scope.ENUM_CONSTANT;
}
// ==================== ASSIGNMENT MANAGEMENT ====================
/**
* Add an assignment to this field.
*/
public void addAssignment(AssignmentInfo assignment) {
assignments.add(assignment);
}
/**
* Get all assignments to this field (unmodifiable).
*/
public List<AssignmentInfo> getAssignments() {
return Collections.unmodifiableList(assignments);
}
/**
* Set the declaration assignment (initial value assignment).
*/
public void setDeclarationAssignment(AssignmentInfo assignment) {
this.declarationAssignment = assignment;
}
/**
* Get the declaration assignment (initial value assignment).
* Returns null if there was no initializer or it hasn't been validated yet.
*/
public AssignmentInfo getDeclarationAssignment() {
return declarationAssignment;
}
/**
* Find an assignment that contains the given position.
* Returns null if no assignment contains this position.
* Prioritizes LHS matches over RHS matches.
*/
public AssignmentInfo findAssignmentAtPosition(int position) {
// Check declaration assignment first
if (declarationAssignment != null && declarationAssignment.containsPosition(position)) {
// Prioritize LHS match
if (declarationAssignment.containsLhsPosition(position)) {
return declarationAssignment;
}
}
// Check other assignments for LHS matches first (more specific)
for (AssignmentInfo assign : assignments) {
if (assign.containsLhsPosition(position)) {
return assign;
}
}
// Then check RHS matches (less specific - might just be a reference in the value)
if (declarationAssignment != null && declarationAssignment.containsRhsPosition(position)) {
return declarationAssignment;
}
for (AssignmentInfo assign : assignments) {
if (assign.containsRhsPosition(position)) {
return assign;
}
}
return null;
}
/**
* Get all errored assignments for this field.
*/
public List<AssignmentInfo> getErroredAssignments() {
List<AssignmentInfo> errored = new ArrayList<>();
// Include declaration assignment if it has an error
if (declarationAssignment != null && declarationAssignment.hasError()) {
errored.add(declarationAssignment);
}
// Include all other assignments with errors
for (AssignmentInfo assign : assignments) {
if (assign.hasError()) {
errored.add(assign);
}
}
return errored;
}
/**
* Clear all assignments (for re-parsing).
*/
public void clearAssignments() {
assignments.clear();
declarationAssignment = null;
}
// ==================== MODIFIER CHECKS ====================
/**
* Check if this field is declared as final.
* Works for both script-defined fields (via modifiers) and reflection fields.
*/
public boolean isFinal() {
if (reflectionField != null)
return Modifier.isFinal(reflectionField.getModifiers());
return Modifier.isFinal(modifiers);
}
/**
* Check if this field is declared as static.
*/
public boolean isStatic() {
if (reflectionField != null)
return Modifier.isStatic(reflectionField.getModifiers());
return Modifier.isStatic(modifiers);
}
/**
* Check if this field is declared as private.
*/
public boolean isPrivate() {
if (reflectionField != null)
return Modifier.isPrivate(reflectionField.getModifiers());
return Modifier.isPrivate(modifiers);
}
/**
* Check if this field is declared as protected.
*/
public boolean isProtected() {
if (reflectionField != null)
return Modifier.isProtected(reflectionField.getModifiers());
return Modifier.isProtected(modifiers);
}
/**
* Check if this field is declared as public.
*/
public boolean isPublic() {
if (reflectionField != null)
return Modifier.isPublic(reflectionField.getModifiers());
return Modifier.isPublic(modifiers);
}
/**
* Get the raw modifiers value.
*/
public int getModifiers() {
if (reflectionField != null)
return reflectionField.getModifiers();
return modifiers;
}
/**
* Get the reflection field, if available.
*/
public java.lang.reflect.Field getReflectionField() {
return reflectionField;
}
// ==================== TYPE INFERENCE ====================
/**
* Get the inferred type for this field.
* This is set when a variable declared as "any" has its type refined through
* assignment analysis or JSDoc comments.
*/
public TypeInfo getInferredType() {
return inferredType;
}
/**
* Set the inferred type for this field.
* Used when type inference determines a more specific type than the declared type.
* @param inferredType The refined type based on assignments or JSDoc
*/
public void setVarArg(boolean isVarArg) { this.isVarArg = isVarArg; }
public boolean isVarArg() { return isVarArg; }
public void setInferredType(TypeInfo inferredType) {
this.inferredType = inferredType;
}
/**
* Check if this field has a type that can be refined (currently "any" type).
*/
public boolean canInferType() {
return declaredType != null && "any".equals(declaredType.getFullName());
}
/**
* Get the effective type for this field, considering inference.
* Returns inferredType if available, otherwise declaredType.
*/
public TypeInfo getEffectiveType() {
if (inferredType != null) {
return inferredType;
}
return declaredType;
}
// ==================== BASIC GETTERS ====================
public String getName() { return name; }
public Scope getScope() { return scope; }
public TypeInfo getDeclaredType() { return declaredType; }
public TypeInfo getTypeInfo() {
// Return the effective type (inferred if available, otherwise declared)
return getEffectiveType();
}
public int getDeclarationOffset() { return declarationOffset; }
public boolean isResolved() {
// Check if we have any resolved type (declared or inferred)
TypeInfo effectiveType = getEffectiveType();
return effectiveType != null && effectiveType.isResolved();
}
public MethodInfo getContainingMethod() { return containingMethod; }
public String getDocumentation() { return documentation; }
public JSDocInfo getJSDocInfo() { return jsDocInfo; }
public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; }
public int getInitStart() { return initStart; }
public int getInitEnd() { return initEnd; }
public boolean hasInitializer() { return initStart >= 0 && initEnd > initStart; }
public boolean isGlobal() { return scope == Scope.GLOBAL; }
public boolean isLocal() { return scope == Scope.LOCAL; }
public boolean isParameter() { return scope == Scope.PARAMETER; }
/**
* Check if a reference at the given position can see this field.
* For local variables, they're only visible after their declaration.
*/
public boolean isVisibleAt(int position) {
if (scopeInfo != null && !scopeInfo.containsPosition(position)) {
return false;
}
if (scope == Scope.GLOBAL || scope == Scope.PARAMETER) {
return true; // Always visible in their scope
}
// Local variables are only visible after declaration
return position >= declarationOffset;
}
public ScopeInfo getScopeInfo() { return scopeInfo; }
public void setScopeInfo(ScopeInfo scopeInfo) { this.scopeInfo = scopeInfo; }
/**
* Get the appropriate TokenType for highlighting this field.
*/
public TokenType getTokenType() {
if (!resolved) {
return TokenType.UNDEFINED_VAR;
}
switch (scope) {
case GLOBAL:
// Static final fields get special highlighting
if (isStatic() && isFinal()) {
return TokenType.STATIC_FINAL_FIELD;
}
return TokenType.GLOBAL_FIELD;
case LOCAL:
return TokenType.LOCAL_FIELD;
case PARAMETER:
return TokenType.PARAMETER;
case ENUM_CONSTANT:
return TokenType.ENUM_CONSTANT;
default:
return TokenType.VARIABLE;
}
}
@Override
public String toString() {
return "FieldInfo{" + name + ", " + scope + ", type=" + declaredType + ", final=" + isFinal() + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FieldInfo fieldInfo = (FieldInfo) o;
return name.equals(fieldInfo.name) && scope == fieldInfo.scope;
}
@Override
public int hashCode() {
return name.hashCode() * 31 + scope.ordinal();
}
/**
* Wrapper class that holds both FieldInfo and MethodCallInfo for a variable usage.
* This is used when a variable is used as an argument to a method call.
*/
public static class ArgInfo {
public final FieldInfo fieldInfo;
public final MethodCallInfo methodCallInfo;
public ArgInfo(FieldInfo fieldInfo, MethodCallInfo methodCallInfo) {
this.fieldInfo = fieldInfo;
this.methodCallInfo = methodCallInfo;
}
}
}
@@ -0,0 +1,304 @@
package noppes.npcs.client.gui.util.script.interpreter.hover;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.util.ResourceLocation;
import noppes.npcs.client.ClientProxy;
import noppes.npcs.client.gui.util.GuiUtil;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
import java.util.List;
/**
* Renders gutter icons for method inheritance (override/implements) in the script editor.
*
* Renders IntelliJ-style tooltips when hovering over gutter icons:
* - Background + border matching TokenHoverRenderer style
* - Colored declaration showing which class/interface the method overrides/implements
* - Uses the same token highlighting system for type names
*/
public class GutterIconRenderer {
// ==================== CONSTANTS ====================
/** Texture resource for script icons (64x32: first 32x32 = override, second 32x32 = implements) */
private static final ResourceLocation SCRIPT_ICONS = new ResourceLocation("customnpcs",
"textures/gui/script/icons.png");
/** Icon size when rendered in the gutter (scaled from 32x32) */
private static final int GUTTER_ICON_SIZE = 10;
/** Extra gutter width to accommodate inheritance icons */
public static final int ICON_GUTTER_WIDTH = 12;
/** Padding inside the tooltip box */
private static final int PADDING = 6;
/** Line spacing between rows */
private static final int LINE_SPACING = 2;
/** Vertical offset from mouse */
private static final int VERTICAL_OFFSET = 10;
// ==================== COLORS ====================
/** Background color (dark gray like IntelliJ) */
private static final int BG_COLOR = 0xF0313335;
/** Border color */
private static final int BORDER_COLOR = 0xFF3C3F41;
/** Info text color */
private static final int INFO_COLOR = 0xFFA9B7C6;
// ==================== RENDERING ====================
/**
* Render gutter icons for a range of lines.
*
* @param lineHeight Height of each line
* @param gutterX X position for the icon in the gutter
* @param gutterY Base Y position of the gutter
* @param renderStart First line index to render
* @param renderEnd Last line index to render
* @param scrolledLine Current scroll position
* @param stringYOffset Y offset for text positioning
* @param methods All methods in the document
* @param lines Line data for position calculation
* @param xMouse Mouse X position
* @param yMouse Mouse Y position
* @param fracPixels Fractional scroll offset
* @return The hovered method, or null if no icon is hovered
*/
public static MethodInfo renderIcons(
int lineHeight,
int gutterX,
int gutterY,
int renderStart,
int renderEnd,
int scrolledLine,
int stringYOffset,
List<MethodInfo> methods,
List<?> lines,
int xMouse,
int yMouse,
float fracPixels) {
if (methods == null || methods.isEmpty())
return null;
MethodInfo hoveredMethod = null;
float adjustedMouseY = yMouse + fracPixels;
for (int lineIndex = renderStart; lineIndex <= renderEnd; lineIndex++) {
MethodInfo method = getMethodAtLine(lineIndex, methods, lines);
if (method == null || !method.hasInheritanceMarker())
continue;
int posY = gutterY + (lineIndex - scrolledLine) * lineHeight + stringYOffset;
int iconY = posY + (lineHeight - GUTTER_ICON_SIZE) / 2 - 1;
// Draw the icon
renderIcon(gutterX, iconY, method.isOverride());
// Check for hover
int iconScaleOffsetX = -4;
int screenPosY = gutterY + (lineIndex - scrolledLine) * lineHeight;
if (xMouse >= gutterX + iconScaleOffsetX && xMouse < gutterX + GUTTER_ICON_SIZE + iconScaleOffsetX &&
adjustedMouseY >= screenPosY && adjustedMouseY < screenPosY + lineHeight) {
hoveredMethod = method;
}
}
return hoveredMethod;
}
/**
* Render a single gutter icon.
*/
private static void renderIcon(int x, int y, boolean isOverride) {
int iconU = isOverride ? 0 : 32;
Minecraft.getMinecraft().renderEngine.bindTexture(SCRIPT_ICONS);
GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
GL11.glPushMatrix();
float scale = 2.0f, scaleOffsetX = -4, scaleOffsetY = -3.25f;
GL11.glScalef(scale, scale, scale);
GL11.glTranslatef(scaleOffsetX, scaleOffsetY, 0);
GuiUtil.drawScaledTexturedRect(
(int) (x / scale), (int) (y / scale),
iconU, 0, 32, 32,
GUTTER_ICON_SIZE, GUTTER_ICON_SIZE,
64, 32
);
GL11.glPopMatrix();
}
/**
* Render the tooltip for a hovered gutter icon.
*/
public static void renderTooltip(MethodInfo method, int mouseX, int mouseY, int viewportX, int viewportWidth,
int viewportY, int viewportHeight) {
if (method == null)
return;
// Build tooltip content
List<TextSegment> segments = buildTooltipContent(method);
if (segments.isEmpty())
return;
int lineHeight = ClientProxy.Font.height();
// Calculate dimensions
int contentWidth = calculateContentWidth(segments);
int contentHeight = lineHeight;
int boxWidth = contentWidth + PADDING * 2;
int boxHeight = contentHeight + PADDING * 2;
// Position tooltip
int tooltipX = mouseX + VERTICAL_OFFSET;
int tooltipY = mouseY - 5;
// Clamp to viewport
int rightBound = viewportX + viewportWidth;
int bottomBound = viewportY + viewportHeight;
if (tooltipX + boxWidth > rightBound) {
tooltipX = mouseX - boxWidth - 5;
}
if (tooltipX < viewportX) {
tooltipX = viewportX;
}
if (tooltipY + boxHeight > bottomBound) {
tooltipY = bottomBound - boxHeight;
}
if (tooltipY < viewportY) {
tooltipY = viewportY;
}
// Render tooltip box
renderTooltipBox(tooltipX, tooltipY, boxWidth, boxHeight, segments);
}
// ==================== PRIVATE HELPERS ====================
/**
* Get the method that starts on the given line.
*/
private static MethodInfo getMethodAtLine(int lineIndex, List<MethodInfo> methods, List<?> lines) {
if (lineIndex < 0 || lineIndex >= lines.size())
return null;
Object lineObj = lines.get(lineIndex);
int lineStart, lineEnd;
try {
java.lang.reflect.Field startField = lineObj.getClass().getField("start");
java.lang.reflect.Field endField = lineObj.getClass().getField("end");
lineStart = startField.getInt(lineObj);
lineEnd = endField.getInt(lineObj);
} catch (Exception e) {
return null;
}
for (MethodInfo method : methods) {
if (!method.hasInheritanceMarker())
continue;
int nameOffset = method.getNameOffset();
if (nameOffset >= lineStart && nameOffset < lineEnd) {
return method;
}
}
return null;
}
/**
* Build the tooltip content as colored text segments.
*/
private static List<TextSegment> buildTooltipContent(MethodInfo method) {
List<TextSegment> segments = new ArrayList<>();
if (method.isOverride()) {
TypeInfo overridesFrom = method.getOverridesFrom();
segments.add(new TextSegment("Overrides method in ", INFO_COLOR));
if (overridesFrom != null) {
int color = TokenType.getColor(overridesFrom);
segments.add(new TextSegment(overridesFrom.getSimpleName(), color));
} else {
segments.add(new TextSegment("parent class", INFO_COLOR));
}
} else if (method.isImplements()) {
TypeInfo implementsFrom = method.getImplementsFrom();
segments.add(new TextSegment("Implements method from ", INFO_COLOR));
if (implementsFrom != null) {
int color = TokenType.getColor(implementsFrom);
segments.add(new TextSegment(implementsFrom.getSimpleName(), color));
} else {
segments.add(new TextSegment("interface", INFO_COLOR));
}
}
return segments;
}
/**
* Calculate the width needed for the segments.
*/
private static int calculateContentWidth(List<TextSegment> segments) {
int totalWidth = 0;
for (TextSegment segment : segments) {
totalWidth += ClientProxy.Font.width(segment.text);
}
return totalWidth;
}
/**
* Render the tooltip box with background, border, and content.
*/
private static void renderTooltipBox(int x, int y, int width, int height, List<TextSegment> segments) {
GL11.glDisable(GL11.GL_SCISSOR_TEST);
// Draw background
int paddedHeight = y + height - 4;
Gui.drawRect(x, y, x + width, paddedHeight, BG_COLOR);
// Draw border
Gui.drawRect(x, y, x + width, y + 1, BORDER_COLOR); // Top
Gui.drawRect(x, paddedHeight - 1, x + width, paddedHeight, BORDER_COLOR); // Bottom
Gui.drawRect(x, y, x + 1, paddedHeight, BORDER_COLOR); // Left
Gui.drawRect(x + width - 1, y, x + width, paddedHeight, BORDER_COLOR); // Right
// Draw text segments
int currentX = x + PADDING;
int currentY = y + PADDING;
for (TextSegment segment : segments) {
ClientProxy.Font.drawString(segment.text, currentX, currentY, 0xFF000000 | segment.color);
currentX += ClientProxy.Font.width(segment.text);
}
}
// ==================== DATA CLASSES ====================
/**
* A colored text segment.
*/
private static class TextSegment {
final String text;
final int color;
TextSegment(String text, int color) {
this.text = text;
this.color = color;
}
}
}
@@ -0,0 +1,558 @@
package noppes.npcs.client.gui.util.script.interpreter.hover;
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
import noppes.npcs.client.gui.util.script.interpreter.ScriptLine;
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
/**
* Manages the hover state for token tooltips in the script editor.
*
* Tracks:
* - Current mouse position
* - Hovered token (if any)
* - Hover duration for delayed tooltip display
* - Whether tooltip should be visible
*
* Implements a 500ms delay before showing tooltips, resetting when
* the mouse moves to a different token.
*/
public class HoverState {
/** Minimum hover time (ms) before showing tooltip */
private static final long HOVER_DELAY_MS = 100;
// ==================== STATE ====================
/** Currently hovered token (null if none) */
private Token hoveredToken;
/** Time when hover on current token started */
private long hoverStartTime;
/** Whether the tooltip should currently be displayed */
private boolean tooltipVisible;
/** Cached hover info for the current token */
private TokenHoverInfo hoverInfo;
/** Last known mouse position */
private int lastMouseX;
private int lastMouseY;
/** Position of the hovered token (for tooltip positioning) */
private int tokenScreenX;
private int tokenScreenY;
private int tokenWidth;
/** Tooltip bounding box (set after each render frame, used for mouse-over detection) */
private int tooltipBoxX, tooltipBoxY, tooltipBoxWidth, tooltipBoxHeight;
/** Also store the actual tooltip panel rect (not including the token-gap extension) */
private int tooltipPanelX, tooltipPanelY, tooltipPanelW, tooltipPanelH;
/** Scrollbar thumb rect (set each render frame, used for drag/hover detection) */
private int scrollbarThumbX, scrollbarThumbY, scrollbarThumbH;
private int scrollbarX;
private int scrollbarTrackTop;
private int scrollbarTrackHeight;
private boolean isDraggingScrollbar;
private int dragStartMouseY;
private int dragStartScrollOffset;
/** Tooltip panel drag state */
private boolean isDraggingTooltip;
private int tooltipDragOffsetX, tooltipDragOffsetY;
private boolean tooltipPositionOverridden;
private int overriddenTooltipX, overriddenTooltipY;
/** Tooltip panel resize state */
private boolean isResizingTooltip;
private int resizeInitMouseX, resizeInitMouseY;
private int resizeInitW, resizeInitH;
private boolean tooltipSizeOverridden;
private int overriddenTooltipW, overriddenTooltipH;
/** Minimum tooltip dimensions when user-resized */
private static final int MIN_TOOLTIP_W = 80;
private static final int MIN_TOOLTIP_H = 30;
/** Smooth scroll: target offset (pixels), current is animated toward it each frame */
private float targetScrollOffset;
/** Vertical scroll offset for tooltip content (pixels) */
private float tooltipScrollOffsetF;
/** Maximum scroll offset = totalContentHeight - visibleContentHeight (pixels) */
private int tooltipMaxScroll;
/** Pinned token (set when user clicks a token to keep tooltip open) */
private Token pinnedToken;
private TokenHoverInfo pinnedHoverInfo;
/** Whether click-to-pin behaviour is enabled for this hover state. */
private boolean clickToPinEnabled = true;
// ==================== UPDATE ====================
/**
* Update the hover state based on current mouse position.
* Should be called every frame from drawTextBox.
*
* @param mouseX Current mouse X position
* @param mouseY Current mouse Y position
* @param document The script document
* @param viewportX X position of the text viewport (after gutter)
* @param viewportY Y position of the text viewport
* @param viewportWidth Width of the text viewport
* @param viewportHeight Height of the text viewport
* @param scrollOffset Current vertical scroll offset (in lines)
* @param lineHeight Height of each line in pixels
* @param gutterWidth Width of the line number gutter
*/
public void update(int mouseX, int mouseY, ScriptDocument document,
int viewportX, int viewportY, int viewportWidth, int viewportHeight,
float scrollOffset, int lineHeight, int gutterWidth) {
lastMouseX = mouseX;
lastMouseY = mouseY;
// Check if mouse is within the text viewport
if (mouseX < viewportX || mouseX > viewportX + viewportWidth ||
mouseY < viewportY || mouseY > viewportY + viewportHeight) {
clearHover();
return;
}
if (document == null) {
clearHover();
return;
}
// Calculate which line the mouse is over
int relativeY = mouseY - viewportY;
int lineIndex = (int) (scrollOffset + (relativeY / (float) lineHeight));
// Get the line
ScriptLine line = null;
for (ScriptLine l : document.getLines()) {
if (l.getLineIndex() == lineIndex) {
line = l;
break;
}
}
if (line == null) {
clearHover();
return;
}
// Calculate character position within the line
int relativeX = mouseX - viewportX;
int globalPos = line.getGlobalStart() + getCharacterIndexAtX(line, relativeX);
// Find the token at this position
Token token = line.getTokenAt(globalPos);
if (token == null) {
clearHover();
return;
}
// Check if this is a new token
if (token != hoveredToken) {
// New token - reset timer
hoveredToken = token;
hoverStartTime = System.currentTimeMillis();
tooltipVisible = false;
hoverInfo = null;
resetTooltipScroll();
// Calculate token screen position for tooltip
calculateTokenPosition(line, token, viewportX, viewportY, scrollOffset, lineHeight);
} else {
// Same token - check if delay has elapsed
long elapsed = System.currentTimeMillis() - hoverStartTime;
if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) {
tooltipVisible = true;
hoverInfo = TokenHoverInfo.fromToken(token);
}
}
}
/**
* Update the hover state with a specific token.
* Simplified version for when the token has already been found.
*
* @param mouseX Current mouse X position
* @param mouseY Current mouse Y position
* @param token The token at the mouse position (or null if none)
* @param tokenX Screen X position of the token
* @param tokenY Screen Y position of the token
* @param tokenW Width of the token in pixels
*/
public void update(int mouseX, int mouseY, Token token, int tokenX, int tokenY, int tokenW) {
lastMouseX = mouseX;
lastMouseY = mouseY;
clickToPinEnabled=false;
// If a token has been pinned by click, ignore mouse movement updates
if (pinnedToken != null) {
// keep pinned tooltip visible
tooltipVisible = true;
hoverInfo = pinnedHoverInfo;
return;
}
if (token == null) {
clearHover();
return;
}
// Check if this is a new token
if (token != hoveredToken) {
// New token - reset timer
hoveredToken = token;
hoverStartTime = System.currentTimeMillis();
tooltipVisible = false;
hoverInfo = null;
resetTooltipScroll();
// Store token position
tokenScreenX = tokenX;
tokenScreenY = tokenY;
tokenWidth = tokenW;
} else {
// Same token - check if delay has elapsed
long elapsed = System.currentTimeMillis() - hoverStartTime;
if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) {
tooltipVisible = true;
hoverInfo = TokenHoverInfo.fromToken(token);
}
}
}
/**
* Clear the current hover state.
*/
public void clearHover() {
if (hoveredToken != null) {
hoveredToken = null;
hoverStartTime = 0;
// Do not clear tooltipInfo here if pinned; if not pinned, hide tooltip.
if (pinnedToken == null) {
tooltipVisible = false;
hoverInfo = null;
tooltipBoxX = tooltipBoxY = tooltipBoxWidth = tooltipBoxHeight = 0;
tooltipPanelX = tooltipPanelY = tooltipPanelW = tooltipPanelH = 0;
isDraggingScrollbar = false;
isDraggingTooltip = false;
tooltipPositionOverridden = false;
isResizingTooltip = false;
tooltipSizeOverridden = false;
tooltipScrollOffsetF = 0;
targetScrollOffset = 0;
tooltipMaxScroll = 0;
isDraggingScrollbar = false;
}
}
}
/**
* Force the tooltip to hide (e.g., when clicking).
*/
public void hideTooltip() {
tooltipVisible = false;
}
/**
* Enable or disable click-to-pin behaviour.
*/
public void setClickToPinEnabled(boolean enabled) {
this.clickToPinEnabled = enabled;
}
public boolean isClickToPinEnabled() {
return clickToPinEnabled;
}
/**
* Pin a token so its tooltip stays visible until explicitly unpinned.
*/
public void pinToken(Token token, int tokenX, int tokenY, int tokenW) {
if (token == null) return;
this.pinnedToken = token;
this.pinnedHoverInfo = TokenHoverInfo.fromToken(token);
this.tooltipVisible = pinnedHoverInfo != null && pinnedHoverInfo.hasContent();
this.tokenScreenX = tokenX;
this.tokenScreenY = tokenY;
this.tokenWidth = tokenW;
// ensure hoveredToken reflects pinned token
this.hoveredToken = token;
}
/**
* Unpin any pinned token and hide tooltip.
*/
public void unpin() {
this.pinnedToken = null;
this.pinnedHoverInfo = null;
this.tooltipVisible = false;
this.hoverInfo = null;
}
public boolean isPinned() { return pinnedToken != null; }
// ==================== POSITION CALCULATION ====================
/**
* Get the character index within a line at the given X pixel position.
*/
private int getCharacterIndexAtX(ScriptLine line, int x) {
String text = line.getText();
if (text == null || text.isEmpty()) return 0;
int accumWidth = 0;
for (int i = 0; i < text.length(); i++) {
int charWidth = noppes.npcs.client.ClientProxy.Font.width(String.valueOf(text.charAt(i)));
if (accumWidth + charWidth / 2 > x) {
return i;
}
accumWidth += charWidth;
}
return text.length();
}
/**
* Calculate the screen position of a token for tooltip positioning.
*/
private void calculateTokenPosition(ScriptLine line, Token token,
int viewportX, int viewportY,
float scrollOffset, int lineHeight) {
// X position: calculate width of text before the token
String lineText = line.getText();
int tokenLocalStart = token.getGlobalStart() - line.getGlobalStart();
tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, lineText.length()));
String textBefore = lineText.substring(0, tokenLocalStart);
tokenScreenX = viewportX + noppes.npcs.client.ClientProxy.Font.width(textBefore);
// Y position: line position minus scroll
int lineY = line.getLineIndex();
tokenScreenY = viewportY + (int) ((lineY - scrollOffset) * lineHeight);
// Token width
tokenWidth = noppes.npcs.client.ClientProxy.Font.width(token.getText());
}
public void setTooltipBounds(int x, int y, int width, int height) {
tooltipBoxX = x;
tooltipBoxY = y;
tooltipBoxWidth = width;
tooltipBoxHeight = height;
}
public void setTooltipMaxScroll(int maxScroll) {
this.tooltipMaxScroll = Math.max(0, maxScroll);
}
public boolean isMouseOverTooltip(int mouseX, int mouseY) {
if (!tooltipVisible || tooltipBoxWidth <= 0) return false;
return mouseX >= tooltipBoxX && mouseX <= tooltipBoxX + tooltipBoxWidth
&& mouseY >= tooltipBoxY && mouseY <= tooltipBoxY + tooltipBoxHeight;
}
public void scrollTooltip(int wheelDelta) {
// LWJGL wheelDelta: positive = scrolled up (content should move up = offset increases)
// Divide by 2 for sensitivity; negate to match expected direction
targetScrollOffset = Math.max(0, Math.min(tooltipMaxScroll, targetScrollOffset - wheelDelta / 5.0f));
}
public int getTooltipScrollOffset() {
return (int) tooltipScrollOffsetF;
}
public void resetTooltipScroll() {
tooltipScrollOffsetF = 0;
targetScrollOffset = 0;
}
/** Lerp current offset toward target. Call every frame while tooltip is visible. */
public void updateSmoothScroll() {
float diff = targetScrollOffset - tooltipScrollOffsetF;
if (Math.abs(diff) < 0.5f) {
tooltipScrollOffsetF = targetScrollOffset;
} else {
tooltipScrollOffsetF += diff * 0.04f;
}
}
/** Called by renderer each frame to store thumb bounds for drag/hover detection. */
public void setScrollbarThumb(int barX, int thumbY, int thumbH, int trackTop, int trackHeight) {
scrollbarX = barX;
scrollbarThumbX = barX;
scrollbarThumbY = thumbY;
scrollbarThumbH = thumbH;
scrollbarTrackTop = trackTop;
scrollbarTrackHeight = trackHeight;
}
public boolean isMouseOverScrollbarThumb(int mx, int my) {
if (!tooltipVisible || scrollbarThumbH <= 0) return false;
return mx >= scrollbarX && mx <= scrollbarX + 3
&& my >= scrollbarThumbY && my <= scrollbarThumbY + scrollbarThumbH;
}
public void startScrollbarDrag(int mouseY) {
isDraggingScrollbar = true;
dragStartMouseY = mouseY;
dragStartScrollOffset = (int) tooltipScrollOffsetF;
}
public void updateScrollbarDrag(int mouseY) {
if (!isDraggingScrollbar || scrollbarTrackHeight <= 0 || tooltipMaxScroll <= 0) return;
float scrollRatio = (float) (scrollbarTrackHeight) / Math.max(1, scrollbarTrackHeight + tooltipMaxScroll);
int thumbH = Math.max(6, (int)(scrollbarTrackHeight * scrollRatio));
int effectiveTrack = scrollbarTrackHeight - thumbH;
if (effectiveTrack <= 0) return;
int deltaY = mouseY - dragStartMouseY;
float scrollDelta = (float) deltaY / effectiveTrack * tooltipMaxScroll;
targetScrollOffset = Math.max(0, Math.min(tooltipMaxScroll, dragStartScrollOffset + scrollDelta));
tooltipScrollOffsetF = targetScrollOffset; // snap during drag
}
public void releaseScrollbarDrag() {
isDraggingScrollbar = false;
}
// ==================== TOOLTIP PANEL DRAG ====================
/** Record the actual rendered tooltip panel rect (used for drag hit-testing). */
public void setTooltipPanel(int x, int y, int w, int h) {
tooltipPanelX = x;
tooltipPanelY = y;
tooltipPanelW = w;
tooltipPanelH = h;
}
public boolean isMouseOverTooltipPanel(int mx, int my) {
if (!tooltipVisible || tooltipPanelW <= 0) return false;
return mx >= tooltipPanelX && mx <= tooltipPanelX + tooltipPanelW
&& my >= tooltipPanelY && my <= tooltipPanelY + tooltipPanelH;
}
public int getTooltipPanelX() { return tooltipPanelX; }
public int getTooltipPanelY() { return tooltipPanelY; }
/** Begin dragging the tooltip; offsets are from mouse to tooltip top-left. */
public void startTooltipDrag(int mouseX, int mouseY) {
isDraggingTooltip = true;
tooltipDragOffsetX = mouseX - tooltipPanelX;
tooltipDragOffsetY = mouseY - tooltipPanelY;
}
/** Called every frame while M1 is held to move the tooltip. */
public void updateTooltipDrag(int mouseX, int mouseY) {
if (!isDraggingTooltip) return;
overriddenTooltipX = mouseX - tooltipDragOffsetX;
overriddenTooltipY = mouseY - tooltipDragOffsetY;
tooltipPositionOverridden = true;
}
public void releaseTooltipDrag() {
isDraggingTooltip = false;
}
public boolean isDraggingTooltip() { return isDraggingTooltip; }
public boolean hasOverriddenPosition() { return tooltipPositionOverridden; }
public int getOverriddenTooltipX() { return overriddenTooltipX; }
public int getOverriddenTooltipY() { return overriddenTooltipY; }
// ==================== TOOLTIP PANEL RESIZE ====================
/** Returns true when the mouse is over the resize handle (bottom-right corner). */
public boolean isMouseOverResizeHandle(int mx, int my) {
if (!tooltipVisible || tooltipPanelW <= 0) return false;
int rx = tooltipPanelX + tooltipPanelW - 8;
int ry = tooltipPanelY + tooltipPanelH - 8;
return mx >= rx && mx <= tooltipPanelX + tooltipPanelW
&& my >= ry && my <= tooltipPanelY + tooltipPanelH;
}
/** Begin resizing; captures current panel dimensions as the baseline. */
public void startTooltipResize(int mouseX, int mouseY) {
isResizingTooltip = true;
resizeInitMouseX = mouseX;
resizeInitMouseY = mouseY;
resizeInitW = tooltipSizeOverridden ? overriddenTooltipW : tooltipPanelW;
resizeInitH = tooltipSizeOverridden ? overriddenTooltipH : tooltipPanelH;
}
/** Called every frame while M1 is held to resize the tooltip panel. */
public void updateTooltipResize(int mouseX, int mouseY) {
if (!isResizingTooltip) return;
overriddenTooltipW = Math.max(MIN_TOOLTIP_W, resizeInitW + (mouseX - resizeInitMouseX));
overriddenTooltipH = Math.max(MIN_TOOLTIP_H, resizeInitH + (mouseY - resizeInitMouseY));
tooltipSizeOverridden = true;
}
public void releaseTooltipResize() {
isResizingTooltip = false;
}
public boolean isResizingTooltip() { return isResizingTooltip; }
public boolean hasOverriddenSize() { return tooltipSizeOverridden; }
public int getOverriddenTooltipW() { return overriddenTooltipW; }
public int getOverriddenTooltipH() { return overriddenTooltipH; }
public boolean isDraggingScrollbar() {
return isDraggingScrollbar;
}
public int getScrollbarThumbY() { return scrollbarThumbY; }
public int getScrollbarThumbH() { return scrollbarThumbH; }
public int getScrollbarX() { return scrollbarX; }
// ==================== GETTERS ====================
public boolean isTooltipVisible() {
return tooltipVisible && hoverInfo != null && hoverInfo.hasContent();
}
public TokenHoverInfo getHoverInfo() {
return hoverInfo;
}
public Token getHoveredToken() {
return hoveredToken;
}
public int getTokenScreenX() {
return tokenScreenX;
}
public int getTokenScreenY() {
return tokenScreenY;
}
public int getTokenWidth() {
return tokenWidth;
}
public int getLastMouseX() {
return lastMouseX;
}
public int getLastMouseY() {
return lastMouseY;
}
public void setLastMousePosition(int x, int y) {
lastMouseX = x;
lastMouseY = y;
}
/**
* Get the progress (0.0 to 1.0) of the hover delay.
* Can be used for fade-in animation.
*/
public float getHoverProgress() {
if (hoveredToken == null) return 0f;
long elapsed = System.currentTimeMillis() - hoverStartTime;
return Math.min(1.0f, elapsed / (float) HOVER_DELAY_MS);
}
}
@@ -0,0 +1,744 @@
package noppes.npcs.client.gui.util.script.interpreter.hover;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.ScaledResolution;
import noppes.npcs.client.ClientProxy;
import noppes.npcs.client.gui.util.GuiNPCInterface;
import org.lwjgl.opengl.GL11;
import java.util.ArrayList;
import java.util.List;
/**
* Renders hover tooltips for tokens in the script editor.
*
* Renders IntelliJ-style tooltips with:
* - Package name in gray
* - Icon indicator (C/I/E/m/f/v/p)
* - Colored declaration line
* - Documentation/Javadoc
* - Error messages in red
*
* Uses a unified approach for width/height calculation based on max available width.
*/
public class TokenHoverRenderer {
// ==================== CONSTANTS ====================
/** Padding inside the tooltip box */
private static final int PADDING = 6;
/** Line spacing between rows */
private static final int LINE_SPACING = 2;
/** Separator line height */
private static final int SEPARATOR_HEIGHT = 1;
/** Spacing above and below separator */
private static final int SEPARATOR_SPACING = 5;
/** Vertical offset from token */
private static final int VERTICAL_OFFSET = 4;
/** Maximum tooltip width as percentage of viewport */
private static final float MAX_WIDTH_RATIO = 0.9f;
/** Minimum tooltip width */
private static final int MIN_WIDTH = 50;
// ==================== COLORS ====================
/** Background color (dark gray like IntelliJ) */
private static final int BG_COLOR = 0xF0313335;
/** Border color */
private static final int BORDER_COLOR = 0xFF3C3F41;
/** Package text color */
private static int PACKAGE_COLOR = 0xFF6490e2;
/** Error text color */
private static final int ERROR_COLOR = 0xFFFF6B68;
/** Info text color */
private static final int INFO_COLOR = 0xFF808080;
/** Documentation text color */
private static final int DOC_COLOR = 0xFFA9B7C6;
/** Background fill for ``` code blocks */
private static final int CODE_BG_COLOR = 0xFF1E1F22;
/** Left accent bar color for code blocks */
private static final int CODE_ACCENT_COLOR = 0xFF4FC1FF;
/** Horizontal padding inside code block */
private static final int CODE_INDENT = 10;
/** Vertical padding above/below code block content */
private static final int CODE_VPAD = 6;
/** Fixed visual gap between last content line and the panel bottom (always visible, regardless of scroll) */
private static final int BOTTOM_GAP = 4;
// ==================== RENDERING ====================
/**
* Render the hover tooltip.
*/
public static void render(HoverState hoverState, int viewportX, int viewportWidth, int viewportY, int viewportHeight, GuiNPCInterface gui) {
if (!hoverState.isTooltipVisible()) return;
hoverState.updateSmoothScroll();
TokenHoverInfo info = hoverState.getHoverInfo();
if (info == null || !info.hasContent()) return;
int lineHeight = ClientProxy.Font.height();
int tokenX = hoverState.getTokenScreenX();
int tokenY = hoverState.getTokenScreenY();
// Screen dimensions for clamping when the panel has been dragged outside the editor viewport
Minecraft mc2 = Minecraft.getMinecraft();
ScaledResolution sr2 = new ScaledResolution(mc2, mc2.displayWidth, mc2.displayHeight);
int screenW = sr2.getScaledWidth();
int screenH = sr2.getScaledHeight();
boolean positionOverridden = hoverState.hasOverriddenPosition();
// Use screen bounds whenever the panel has been dragged OR resized — both escape the editor viewport
boolean useScreenBounds = positionOverridden || hoverState.hasOverriddenSize();
// Calculate max available width for content
int maxContentWidth = getMaxContentWidth(viewportX, viewportWidth, tokenX);
// Calculate actual content width needed (clamped to max)
int contentWidth = calculateContentWidth(info, maxContentWidth);
// Cap visible height at 60% of viewport
int totalContentHeight = calculateContentHeight(info, contentWidth);
int maxVisibleContentHeight = (int)((useScreenBounds ? screenH : viewportHeight) * 0.60f);
if (totalContentHeight > maxVisibleContentHeight) {
// Scrollbar will appear, reducing effective wrap width by 6px — re-measure for accurate height
totalContentHeight = calculateContentHeight(info, contentWidth - 6);
}
int visibleContentHeight = Math.min(totalContentHeight, maxVisibleContentHeight);
// Box dimensions — use visibleContentHeight so positioning matches rendering
int boxWidth = contentWidth + PADDING * 2;
int boxHeight = visibleContentHeight + PADDING * 2;
// Apply user size override when the panel has been resized
if (hoverState.hasOverriddenSize()) {
int overW = Math.max(MIN_WIDTH + PADDING * 2, hoverState.getOverriddenTooltipW());
int overH = Math.max(lineHeight + PADDING * 2, hoverState.getOverriddenTooltipH());
boxWidth = overW;
visibleContentHeight = overH - PADDING * 2;
boxHeight = overH;
// Re-measure with new wrap width so scrollbar detection is accurate
int newContentW = boxWidth - PADDING * 2;
totalContentHeight = calculateContentHeight(info, newContentW);
if (totalContentHeight > visibleContentHeight) {
totalContentHeight = calculateContentHeight(info, newContentW - 6);
}
}
// Position the tooltip — use dragged position if user has panned the panel
int tooltipX = positionOverridden ? hoverState.getOverriddenTooltipX() : tokenX;
int tooltipY = positionOverridden ? hoverState.getOverriddenTooltipY() : tokenY + lineHeight + VERTICAL_OFFSET;
// Clamp position — use screen bounds when user has dragged the panel outside the editor viewport
int leftBound = useScreenBounds ? 0 : viewportX;
int rightBound = useScreenBounds ? screenW : viewportX + viewportWidth;
int topBound = useScreenBounds ? 0 : viewportY;
int bottomBound = useScreenBounds ? screenH : viewportY + viewportHeight;
if (tooltipX + boxWidth > rightBound) {
tooltipX = rightBound - boxWidth;
}
if (tooltipX < leftBound) {
tooltipX = leftBound;
}
// If box still doesn't fit horizontally, shrink it
int availableWidth = rightBound - tooltipX;
if (boxWidth > availableWidth) {
boxWidth = availableWidth;
}
if (tooltipY + boxHeight > bottomBound) {
if (useScreenBounds) {
tooltipY = bottomBound - boxHeight;
} else {
tooltipY = tokenY - boxHeight - VERTICAL_OFFSET; // try above token
}
}
if (tooltipY < topBound) {
tooltipY = topBound;
}
// Record actual panel rect for drag hit-testing
hoverState.setTooltipPanel(tooltipX, tooltipY, boxWidth, boxHeight);
// Mouse-over detection bounds:
// When dragged, use panel rect only — no gap extension (token is unrelated to panel position)
// When default, union the token bottom and the tooltip rect so both directions are covered
if (hoverState.hasOverriddenPosition()) {
hoverState.setTooltipBounds(tooltipX, tooltipY, boxWidth, boxHeight);
} else {
int boundsTopY = Math.min(tokenY + lineHeight, tooltipY);
int boundsBottomY = Math.max(tooltipY + boxHeight, tokenY + lineHeight);
hoverState.setTooltipBounds(tooltipX, boundsTopY, boxWidth, boundsBottomY - boundsTopY);
}
// Render the tooltip
renderTooltipBox(tooltipX, tooltipY, boxWidth, maxContentWidth, info, hoverState,
totalContentHeight, visibleContentHeight, gui);
}
/**
* Calculate the maximum content width based on viewport and token position.
*/
private static int getMaxContentWidth(int viewportX, int viewportWidth, int tokenX) {
// Max width is based on viewport size (80% by default)
// This scales naturally with viewport size - no artificial hard cap
int maxWidth = (int)(viewportWidth * MAX_WIDTH_RATIO);
// The tooltip can expand across the entire viewport width
// (positioning logic will shift it left if needed)
return Math.max(MIN_WIDTH, maxWidth);
}
/**
* Render the tooltip box with all content.
*/
private static void renderTooltipBox(int x, int y, int boxWidth, int wrapWidth, TokenHoverInfo info,
HoverState hoverState, int totalContentHeight, int visibleContentHeight, GuiNPCInterface gui) {
boolean hasScrollbar = totalContentHeight > visibleContentHeight;
// Wrap at the actual box content area width, not the max allowed width.
// Using maxContentWidth here caused lines to never wrap at wide viewports (fullscreen).
int contentWrapWidth = boxWidth - PADDING * 2;
int effectiveWrapWidth = hasScrollbar ? contentWrapWidth - 6 : contentWrapWidth;
int boxHeight = visibleContentHeight + PADDING * 2;
// Store scroll metadata into HoverState
hoverState.setTooltipMaxScroll(Math.max(0, totalContentHeight - visibleContentHeight + BOTTOM_GAP));
// Draw background
Gui.drawRect(x, y, x + boxWidth, y + boxHeight, BG_COLOR);
// Draw border
Gui.drawRect(x, y, x + boxWidth, y + 1, BORDER_COLOR);
Gui.drawRect(x, y + boxHeight - 1, x + boxWidth, y + boxHeight, BORDER_COLOR);
Gui.drawRect(x, y, x + 1, y + boxHeight, BORDER_COLOR);
Gui.drawRect(x + boxWidth - 1, y, x + boxWidth, y + boxHeight, BORDER_COLOR);
Minecraft mc = Minecraft.getMinecraft();
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
int sf = sr.getScaleFactor();
double panX = gui != null ? gui.getPanX() : 0;
double panY = gui != null ? gui.getPanY() : 0;
int clipX = (int)((x + 1 - panX) * sf);
int clipY = (int)((sr.getScaledHeight() - (y - panY + boxHeight - 1 - BOTTOM_GAP)) * sf);
int clipW = (hasScrollbar ? boxWidth - PADDING - 1 : boxWidth - 2) * sf;
int clipH = (boxHeight - 2 - BOTTOM_GAP) * sf;
GL11.glEnable(GL11.GL_SCISSOR_TEST);
GL11.glScissor(clipX, clipY, clipW, clipH);
int textX = x + PADDING;
int currentY = y + PADDING - hoverState.getTooltipScrollOffset();
int lineHeight = ClientProxy.Font.height();
String packageName = info.getPackageName();
List<TokenHoverInfo.TextSegment> declaration = info.getDeclaration();
List<String> docs = info.getDocumentation();
List<String> additionalInfo = info.getAdditionalInfo();
// Draw errors first
List<String> errors = info.getErrors();
if (!errors.isEmpty()) {
for (String error : errors) {
List<String> wrappedLines = wrapText(error, effectiveWrapWidth);
for (String line : wrappedLines) {
drawText(textX, currentY, line, ERROR_COLOR);
currentY += lineHeight + LINE_SPACING;
}
}
boolean onlyErrors = errors != null && !errors.isEmpty() && (packageName == null || packageName.isEmpty()) && (declaration == null ||
declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty());
if (!onlyErrors) { //Add error separator line
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING - 5;
Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR);
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
}
currentY += LINE_SPACING;
}
// Draw package name
if (packageName != null && !packageName.isEmpty()) {
String packageText = "\u25CB " + packageName;
List<String> wrappedLines = wrapText(packageText, effectiveWrapWidth);
for (String line : wrappedLines) {
drawText(textX, currentY, line, PACKAGE_COLOR);
currentY += lineHeight + LINE_SPACING;
}
}
// Draw declaration (colored segments with wrapping)
if (!declaration.isEmpty()) {
currentY = drawWrappedSegments(textX, currentY, effectiveWrapWidth, declaration);
currentY += LINE_SPACING;
}
// Draw documentation (plain text)
if (!docs.isEmpty()) {
// Draw separator line before documentation
Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR);
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
for (String doc : docs) {
List<String> wrappedLines = wrapText(doc, effectiveWrapWidth);
for (String line : wrappedLines) {
drawText(textX, currentY, line, DOC_COLOR);
currentY += lineHeight + LINE_SPACING;
}
}
}
// Draw JSDoc-formatted documentation with colored segments
List<TokenHoverInfo.DocumentationLine> jsDocLines = info.getJSDocLines();
if (!jsDocLines.isEmpty()) {
// Draw separator line if there was plain documentation or declaration
if (docs.isEmpty() && !declaration.isEmpty()) {
Gui.drawRect(textX, currentY, x + boxWidth - PADDING, currentY + SEPARATOR_HEIGHT, BORDER_COLOR);
currentY += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
}
boolean previousVisibleJSDocWasCode = false;
for (int di = 0; di < jsDocLines.size(); di++) {
TokenHoverInfo.DocumentationLine docLine = jsDocLines.get(di);
if (docLine.isCodeBlockFirst) {
// Lookahead: measure full block height
int blockH = CODE_VPAD;
for (int k = di; k < jsDocLines.size() && jsDocLines.get(k).isCodeLine; k++) {
TokenHoverInfo.DocumentationLine cl = jsDocLines.get(k);
if (!cl.isEmpty()) {
int clIndent = cl.codeLeadingSpaces * ClientProxy.Font.width(" ");
int clWrapW = Math.max(10, effectiveWrapWidth - CODE_INDENT - clIndent);
blockH += calculateSegmentsHeight(clWrapW, cl.segments) + LINE_SPACING;
} else {
blockH += lineHeight / 2;
}
}
blockH -= LINE_SPACING * 2; // subtract trailing LINE_SPACING (present inside bg) + one more to visually balance top/bottom
blockH += CODE_VPAD;
// Draw background
Gui.drawRect(textX , currentY - CODE_VPAD,
textX + effectiveWrapWidth + 4, currentY - CODE_VPAD + blockH,
CODE_BG_COLOR);
// Left accent bar
Gui.drawRect(textX, currentY - CODE_VPAD,
textX+2, currentY - CODE_VPAD + blockH,
CODE_ACCENT_COLOR);
}
if (docLine.isCodeLine) {
if (!docLine.isEmpty()) {
int explicitIndent = docLine.codeLeadingSpaces * ClientProxy.Font.width(" ");
int codeX = textX + CODE_INDENT + explicitIndent;
int codeWrapW = Math.max(10, effectiveWrapWidth - CODE_INDENT - explicitIndent);
currentY = drawWrappedSegments(codeX, currentY, codeWrapW, docLine.segments);
currentY += LINE_SPACING;
previousVisibleJSDocWasCode = true;
} else {
currentY += lineHeight / 2;
}
} else if (!docLine.isEmpty()) {
if (previousVisibleJSDocWasCode) {
currentY += LINE_SPACING;
}
currentY = drawWrappedSegments(textX, currentY, effectiveWrapWidth, docLine.segments);
currentY += LINE_SPACING;
previousVisibleJSDocWasCode = false;
} else {
currentY += (di > 0 && jsDocLines.get(di - 1).isCodeLine) ? LINE_SPACING : lineHeight / 2;
}
}
}
// Draw additional info
if (!additionalInfo.isEmpty()) {
currentY += LINE_SPACING;
for (String infoLine : additionalInfo) {
List<String> wrappedLines = wrapText(infoLine, effectiveWrapWidth);
for (String line : wrappedLines) {
drawText(textX, currentY, line, INFO_COLOR);
currentY += lineHeight + LINE_SPACING;
}
}
}
// Restore scissor state before drawing scrollbar (scrollbar must not be clipped)
GL11.glDisable(GL11.GL_SCISSOR_TEST);
// Draw scrollbar outside scissor rect so it is never clipped
if (hasScrollbar) {
int scrollbarX = x + boxWidth - PADDING;
int scrollbarTrackTop = y + PADDING;
int scrollbarTrackHeight = visibleContentHeight - 4; // leave room for resize handle
// Track
Gui.drawRect(scrollbarX, scrollbarTrackTop, scrollbarX + 3,
scrollbarTrackTop + scrollbarTrackHeight, 0x40888888);
// Thumb
float scrollRatio = (float) visibleContentHeight / (totalContentHeight + BOTTOM_GAP);
int thumbHeight = Math.max(6, (int)(scrollbarTrackHeight * scrollRatio));
int effectiveMaxScroll = totalContentHeight - visibleContentHeight + BOTTOM_GAP;
// Clamp scrollProgress to [0,1] — guards against stale offset after resize shrinks content
float scrollProgress = effectiveMaxScroll > 0
? Math.min(1f, (float) hoverState.getTooltipScrollOffset() / effectiveMaxScroll) : 0f;
int thumbY = scrollbarTrackTop + (int)((scrollbarTrackHeight - thumbHeight) * scrollProgress);
// Clamp thumb within track bounds defensively
thumbY = Math.max(scrollbarTrackTop, Math.min(scrollbarTrackTop + scrollbarTrackHeight - thumbHeight, thumbY));
// Store thumb bounds in HoverState for drag/hover detection
hoverState.setScrollbarThumb(scrollbarX, thumbY, thumbHeight, scrollbarTrackTop, scrollbarTrackHeight);
// Highlight thumb when hovered or dragging (darker = active, like AutocompleteMenu)
int mouseX = hoverState.getLastMouseX();
int mouseY = hoverState.getLastMouseY();
boolean thumbActive = hoverState.isDraggingScrollbar()
|| hoverState.isMouseOverScrollbarThumb(mouseX, mouseY);
int thumbColor = thumbActive ? 0xFFCCCCCC : 0xFF808080;
Gui.drawRect(scrollbarX, thumbY, scrollbarX + 3, thumbY + thumbHeight, thumbColor);
}
// Draw resize handle at bottom-right corner (outside scissor, same as scrollbar)
{
int mouseX = hoverState.getLastMouseX();
int mouseY = hoverState.getLastMouseY();
boolean resizeActive = hoverState.isResizingTooltip()
|| hoverState.isMouseOverResizeHandle(mouseX, mouseY);
int dotColor = resizeActive ? 0xFFCCCCCC : 0x80888888;
int rhX = x + boxWidth - 2;
int rhY = y + boxHeight - 2;
// Three-dot diagonal pattern (◢)
Gui.drawRect(rhX - 1, rhY - 1, rhX, rhY, dotColor);
Gui.drawRect(rhX - 3, rhY - 1, rhX - 2, rhY, dotColor);
Gui.drawRect(rhX - 1, rhY - 3, rhX, rhY - 2, dotColor);
Gui.drawRect(rhX - 5, rhY - 1, rhX - 4, rhY, dotColor);
Gui.drawRect(rhX - 3, rhY - 3, rhX - 2, rhY - 2, dotColor);
Gui.drawRect(rhX - 1, rhY - 5, rhX, rhY - 4, dotColor);
}
}
/**
* Draw colored text segments with word wrapping.
* Returns the Y position after drawing.
*/
private static int drawWrappedSegments(int startX, int startY, int maxWidth, List<TokenHoverInfo.TextSegment> segments) {
int lineHeight = ClientProxy.Font.height();
int currentX = startX;
int currentY = startY;
for (TokenHoverInfo.TextSegment segment : segments) {
String text = segment.text;
int color = 0xFF000000 | segment.color;
// Split segment into words for wrapping
String[] words = text.split("(?<=\\s)|(?=\\s)"); // Keep whitespace as separate tokens
for (String word : words) {
int wordWidth = ClientProxy.Font.width(word);
// Check if word fits on current line
if (currentX + wordWidth > startX + maxWidth && currentX > startX) {
// Wrap to next line
currentY += lineHeight + LINE_SPACING;
currentX = startX;
// Skip leading whitespace on new line
if (word.trim().isEmpty()) {
continue;
}
}
drawText(currentX, currentY, word, color);
currentX += wordWidth;
}
}
return currentY + lineHeight;
}
/**
* Wrap text to fit within maxWidth.
* Returns list of lines.
*/
private static List<String> wrapText(String text, int maxWidth) {
List<String> lines = new ArrayList<>();
if (text == null || text.isEmpty()) {
return lines;
}
// First split by explicit newlines
String[] paragraphs = text.split("\n", -1); // -1 to preserve trailing empty lines
for (String paragraph : paragraphs) {
if (paragraph.isEmpty()) {
lines.add("");
continue;
}
// Then apply word wrapping to each paragraph
String[] words = paragraph.split(" ");
StringBuilder currentLine = new StringBuilder();
for (String word : words) {
String testLine = currentLine.length() == 0 ? word : currentLine + " " + word;
int testWidth = ClientProxy.Font.width(testLine);
if (testWidth > maxWidth && currentLine.length() > 0) {
lines.add(currentLine.toString());
currentLine = new StringBuilder(word);
} else {
if (currentLine.length() > 0) {
currentLine.append(" ");
}
currentLine.append(word);
}
}
if (currentLine.length() > 0) {
lines.add(currentLine.toString());
}
}
if (lines.isEmpty()) {
lines.add(text);
}
return lines;
}
/**
* Draw text using the font renderer.
*/
private static void drawText(int x, int y, String text, int color) {
ClientProxy.Font.drawString(text, x, y, color);
}
// ==================== DIMENSION CALCULATION ====================
/**
* Calculate the width needed for the content, clamped to maxWidth.
* Returns the width of the longest wrapped line.
*/
private static int calculateContentWidth(TokenHoverInfo info, int maxWidth) {
int longestLineWidth = 0;
// Package name
String packageName = info.getPackageName();
if (packageName != null && !packageName.isEmpty()) {
String packageText = "\u25CB " + packageName;
List<String> wrappedLines = wrapText(packageText, maxWidth);
for (String line : wrappedLines) {
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line));
}
}
// Declaration - wrap segments and find longest line
if (!info.getDeclaration().isEmpty()) {
int declarationLongestLine = calculateSegmentsLongestLine(maxWidth, info.getDeclaration());
longestLineWidth = Math.max(longestLineWidth, declarationLongestLine);
}
// Errors
for (String error : info.getErrors()) {
List<String> wrappedLines = wrapText(error, maxWidth);
for (String line : wrappedLines) {
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line));
}
}
// Documentation
for (String doc : info.getDocumentation()) {
List<String> wrappedLines = wrapText(doc, maxWidth);
for (String line : wrappedLines) {
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(line));
}
}
// JSDoc-formatted documentation lines
List<TokenHoverInfo.DocumentationLine> jsDocLines = info.getJSDocLines();
if (jsDocLines != null) {
for (TokenHoverInfo.DocumentationLine docLine : jsDocLines) {
if (!docLine.isEmpty()) {
int explicitIndent = docLine.isCodeLine
? CODE_INDENT + docLine.codeLeadingSpaces * ClientProxy.Font.width(" ")
: 0;
int lineLongest = calculateSegmentsLongestLine(maxWidth - explicitIndent, docLine.segments) + explicitIndent;
longestLineWidth = Math.max(longestLineWidth, lineLongest);
}
}
}
// Additional info
for (String line : info.getAdditionalInfo()) {
List<String> wrappedLines = wrapText(line, maxWidth);
for (String wrappedLine : wrappedLines) {
longestLineWidth = Math.max(longestLineWidth, ClientProxy.Font.width(wrappedLine));
}
}
// Ensure minimum width
return Math.max(50, longestLineWidth);
}
/**
* Calculate the height needed for the content given the available width.
* Uses the same wrapping logic as rendering.
*/
private static int calculateContentHeight(TokenHoverInfo info, int contentWidth) {
int lineHeight = ClientProxy.Font.height();
int totalHeight = 0;
String packageName = info.getPackageName();
List<TokenHoverInfo.TextSegment> declaration = info.getDeclaration();
List<String> docs = info.getDocumentation();
List<TokenHoverInfo.DocumentationLine> jsDocLines = info.getJSDocLines();
List<String> additionalInfo = info.getAdditionalInfo();
// Errors
List<String> errors = info.getErrors();
if (!errors.isEmpty()) {
for (String error : errors) {
List<String> wrappedLines = wrapText(error, contentWidth);
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
}
boolean onlyErrors = errors != null && !errors.isEmpty() && (packageName == null || packageName.isEmpty()) && (declaration == null ||
declaration.isEmpty()) && (docs == null || docs.isEmpty()) && (jsDocLines == null || jsDocLines.isEmpty()) && (additionalInfo == null || additionalInfo.isEmpty());
if (!onlyErrors) //Add error separator height
totalHeight += (SEPARATOR_SPACING + SEPARATOR_HEIGHT) * 2 - 5;
totalHeight += LINE_SPACING; // Extra space after errors
}
// Package name
if (packageName != null && !packageName.isEmpty()) {
String packageText = "\u25CB " + packageName;
List<String> wrappedLines = wrapText(packageText, contentWidth);
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
}
// Declaration (colored segments with wrapping)
if (!declaration.isEmpty()) {
totalHeight += calculateSegmentsHeight(contentWidth, declaration);
totalHeight += LINE_SPACING;
}
// Documentation (plain text)
if (!docs.isEmpty()) {
// Add space for separator line and spacing
totalHeight += SEPARATOR_SPACING + SEPARATOR_HEIGHT;
for (String doc : docs) {
List<String> wrappedLines = wrapText(doc, contentWidth);
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
}
}
// JSDoc-formatted documentation lines
if (jsDocLines != null && !jsDocLines.isEmpty()) {
if (docs.isEmpty() && !declaration.isEmpty()) {
totalHeight += SEPARATOR_HEIGHT + SEPARATOR_SPACING;
}
boolean previousVisibleJSDocWasCode = false;
for (int i = 0; i < jsDocLines.size(); i++) {
TokenHoverInfo.DocumentationLine docLine = jsDocLines.get(i);
if (!docLine.isEmpty()) {
int explicitIndent = docLine.isCodeLine
? docLine.codeLeadingSpaces * ClientProxy.Font.width(" ")
: 0;
int wrapW = Math.max(10, contentWidth - (docLine.isCodeLine ? CODE_INDENT : 0) - explicitIndent);
if (!docLine.isCodeLine && previousVisibleJSDocWasCode) {
totalHeight += LINE_SPACING;
}
totalHeight += calculateSegmentsHeight(wrapW, docLine.segments);
totalHeight += LINE_SPACING;
previousVisibleJSDocWasCode = docLine.isCodeLine;
} else {
totalHeight += (i > 0 && jsDocLines.get(i - 1).isCodeLine) ? LINE_SPACING : lineHeight / 2;
}
}
}
// Additional info
if (!additionalInfo.isEmpty()) {
totalHeight += LINE_SPACING;
for (String infoLine : additionalInfo) {
List<String> wrappedLines = wrapText(infoLine, contentWidth);
totalHeight += wrappedLines.size() * (lineHeight + LINE_SPACING);
}
}
return Math.max(lineHeight, Math.max(0, totalHeight - LINE_SPACING))-2;
}
/**
* Calculate height needed for wrapped segments.
*/
private static int calculateSegmentsHeight(int maxWidth, List<TokenHoverInfo.TextSegment> segments) {
int lineHeight = ClientProxy.Font.height();
int currentLineWidth = 0;
int lineCount = 1;
for (TokenHoverInfo.TextSegment segment : segments) {
String text = segment.text;
String[] words = text.split("(?<=\\s)|(?=\\s)");
for (String word : words) {
int wordWidth = ClientProxy.Font.width(word);
if (currentLineWidth + wordWidth > maxWidth && currentLineWidth > 0) {
lineCount++;
currentLineWidth = word.trim().isEmpty() ? 0 : wordWidth;
} else {
currentLineWidth += wordWidth;
}
}
}
if (lineCount <= 0) {
return 0;
}
// Total height = (lines * lineHeight) + (spacing between lines)
return (lineCount * lineHeight) + ((lineCount - 1) * LINE_SPACING);
}
/**
* Calculate the width of the longest line when segments are wrapped.
*/
private static int calculateSegmentsLongestLine(int maxWidth, List<TokenHoverInfo.TextSegment> segments) {
int currentLineWidth = 0;
int longestLineWidth = 0;
for (TokenHoverInfo.TextSegment segment : segments) {
String text = segment.text;
String[] words = text.split("(?<=\\s)|(?=\\s)");
for (String word : words) {
int wordWidth = ClientProxy.Font.width(word);
if (currentLineWidth + wordWidth > maxWidth && currentLineWidth > 0) {
// Line break - record current line width and start new line
longestLineWidth = Math.max(longestLineWidth, currentLineWidth);
currentLineWidth = word.trim().isEmpty() ? 0 : wordWidth;
} else {
currentLineWidth += wordWidth;
}
}
}
// Don't forget the last line
longestLineWidth = Math.max(longestLineWidth, currentLineWidth);
return longestLineWidth;
}
}
@@ -0,0 +1,236 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DTSJSDocParser {
private static final Pattern JSDOC_BLOCK_PATTERN = Pattern.compile(
"/\\*\\*\\s*(.*?)\\*/", Pattern.DOTALL);
private static final Pattern TAG_PATTERN = Pattern.compile(
"@(\\w+)(?:\\s+\\{([^}]+)\\})?(?:\\s+(.*))?");
private static final Pattern PARAM_PATTERN = Pattern.compile(
"@param(?:\\s+\\{([^}]+)\\})?(?:\\s+(\\w+))?(?:\\s+(.*))?");
private static final Pattern RETURN_PATTERN = Pattern.compile(
"@returns?\\s+(?:\\{([^}]+)\\}\\s*)?(.*)");
private static final Pattern TYPE_PATTERN = Pattern.compile(
"@type(?:\\s+\\{([^}]+)\\})?(?:\\s+(.*))?");
public static JSDocInfo parseJSDocBlock(String jsDocContent) {
if (jsDocContent == null || jsDocContent.isEmpty()) {
return null;
}
JSDocInfo info = new JSDocInfo(jsDocContent, -1, -1);
String[] lines = jsDocContent.split("\\r?\\n");
StringBuilder descriptionBuilder = new StringBuilder();
boolean foundFirstTag = false;
JSDocTag lastTag = null;
for (String line : lines) {
line = cleanLine(line);
if (line.startsWith("@")) {
foundFirstTag = true;
lastTag = parseTag(line, info);
} else if (!foundFirstTag) {
if (line.isEmpty()) {
// Preserve blank lines as paragraph gaps (only if description has started)
if (descriptionBuilder.length() > 0) {
descriptionBuilder.append("\n");
}
} else {
if (descriptionBuilder.length() > 0) {
descriptionBuilder.append("\n");
}
descriptionBuilder.append(line);
}
} else if (lastTag != null) {
if (!line.isEmpty()) {
String existing = lastTag.getDescription();
if (existing == null || existing.isEmpty()) {
lastTag.setDescription(line);
} else {
lastTag.setDescription(existing + "\n" + line);
}
}
}
}
if (descriptionBuilder.length() > 0) {
info.setDescription(descriptionBuilder.toString().trim());
}
return info;
}
private static String cleanLine(String line) {
// Strip outer leading whitespace (before the * marker) and all trailing whitespace
line = line.trim();
if (line.startsWith("/**")) {
line = line.substring(3).trim();
}
if (line.endsWith("*/")) {
line = line.substring(0, line.length() - 2);
// Strip trailing whitespace between content and */ but preserve leading spaces
int end = line.length();
while (end > 0 && Character.isWhitespace(line.charAt(end - 1))) end--;
line = line.substring(0, end);
}
if (line.startsWith("*")) {
line = line.substring(1); // strip the leading *
// Consume exactly ONE space after * (the JSDoc conventional separator).
// Remaining leading spaces are intentional indentation (e.g. inside code fences).
if (!line.isEmpty() && line.charAt(0) == ' ') {
line = line.substring(1);
}
// Strip trailing whitespace
int end = line.length();
while (end > 0 && Character.isWhitespace(line.charAt(end - 1))) end--;
line = line.substring(0, end);
}
return line;
}
private static JSDocTag parseTag(String line, JSDocInfo info) {
Matcher paramMatcher = PARAM_PATTERN.matcher(line);
if (paramMatcher.find()) {
String type = paramMatcher.group(1);
String name = paramMatcher.group(2);
String desc = paramMatcher.group(3);
if (desc == null && type == null && name != null) {
desc = name;
name = null;
}
if (desc != null) {
desc = desc.trim();
if (desc.isEmpty()) {
desc = null;
}
}
JSDocParamTag tag = JSDocParamTag.create(-1, -1, -1,
type, null, -1, -1, name, -1, -1, desc);
info.addParamTag(tag);
return tag;
}
Matcher returnMatcher = RETURN_PATTERN.matcher(line);
if (returnMatcher.find()) {
String type = returnMatcher.group(1);
String desc = returnMatcher.group(2);
if (desc != null) {
desc = desc.trim();
if (desc.isEmpty()) {
desc = null;
}
}
JSDocReturnTag tag = JSDocReturnTag.create("return", -1, -1, -1,
type, null, -1, -1, desc);
info.setReturnTag(tag);
return tag;
}
Matcher typeMatcher = TYPE_PATTERN.matcher(line);
if (typeMatcher.find()) {
String type = typeMatcher.group(1);
String desc = typeMatcher.group(2);
if (desc != null) {
desc = desc.trim();
if (desc.isEmpty()) {
desc = null;
}
}
JSDocTypeTag tag = JSDocTypeTag.create(-1, -1, -1,
type, null, -1, -1, desc);
info.setTypeTag(tag);
return tag;
}
Matcher tagMatcher = TAG_PATTERN.matcher(line);
if (tagMatcher.find()) {
String tagName = tagMatcher.group(1);
String typeName = tagMatcher.group(2);
String rest = tagMatcher.group(3);
switch (tagName) {
case "see":
JSDocSeeTag seeTag = JSDocSeeTag.createSimple(rest != null ? rest.trim() : "");
info.addSeeTag(seeTag);
return seeTag;
default:
JSDocTag genericTag = new JSDocTag(tagName, -1, -1, -1);
if (typeName != null) {
typeName = typeName.trim();
genericTag.setType(typeName, null, -1, -1);
}
genericTag.setDescription(rest != null ? rest.trim() : "");
info.addTag(genericTag);
return genericTag;
}
}
return null;
}
public static String extractJSDocBefore(String content, int elementStart) {
if (elementStart <= 0) return null;
String searchArea = content.substring(0, elementStart);
int lastJSDocEnd = searchArea.lastIndexOf("*/");
if (lastJSDocEnd < 0) return null;
int jsDocStart = searchArea.lastIndexOf("/**", lastJSDocEnd);
if (jsDocStart < 0) return null;
String between = searchArea.substring(lastJSDocEnd + 2).trim();
if (between.isEmpty() || isOnlyWhitespaceOrModifiers(between)) {
return searchArea.substring(jsDocStart, lastJSDocEnd + 2);
}
return null;
}
private static boolean isOnlyWhitespaceOrModifiers(String text) {
String cleaned = text.replaceAll("\\s+", " ").trim();
return cleaned.isEmpty()
|| cleaned.matches("^(export\\s*)?(readonly\\s*)?(interface|class|type|function)?\\s*$");
}
public static List<JSDocBlock> extractAllJSDocBlocks(String content) {
List<JSDocBlock> blocks = new ArrayList<>();
Matcher matcher = JSDOC_BLOCK_PATTERN.matcher(content);
while (matcher.find()) {
int start = matcher.start();
int end = matcher.end();
String docContent = matcher.group(0);
blocks.add(new JSDocBlock(start, end, docContent));
}
return blocks;
}
public static class JSDocBlock {
public final int start;
public final int end;
public final String content;
public JSDocBlock(int start, int end, String content) {
this.start = start;
this.end = end;
this.content = content;
}
public JSDocInfo parse() {
return parseJSDocBlock(content);
}
}
}
@@ -0,0 +1,201 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.ModContainer;
import java.io.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class DtsModScanner {
private static final Pattern MOD_DTS_PATH_PATTERN = Pattern.compile("^assets/([^/]+)/api/(.+\\.d\\.ts)$");
private static final List<String> DOMAIN_PRIORITY = Arrays.asList("customnpcs", "npcdbc");
private DtsModScanner() {}
static List<DtsFileRef> collectDtsFilesFromMods() {
List<DtsFileRef> dtsFiles = new ArrayList<>();
try {
for (ModContainer mod : Loader.instance().getModList()) {
File source = mod.getSource();
if (source == null || !source.exists()) {
continue;
}
if (source.isDirectory()) {
scanDirectoryForModDts(source, mod.getModId(), dtsFiles);
} else if (source.getName().endsWith(".jar") || source.getName().endsWith(".zip")) {
scanJarForModDts(source, mod.getModId(), dtsFiles);
}
}
} catch (Throwable ignored) {
// Headless/non-FML environment: fall back to resource scanning.
}
return dtsFiles;
}
static void sortDtsFiles(List<DtsFileRef> dtsFiles) {
Collections.sort(dtsFiles, new DtsFileRefComparator());
}
static void logSummary(List<DtsFileRef> dtsFiles) {
Map<String, Integer> domainCounts = new LinkedHashMap<>();
for (DtsFileRef ref : dtsFiles) {
domainCounts.put(ref.getDomain(), domainCounts.getOrDefault(ref.getDomain(), 0) + 1);
}
System.out.println("[JSTypeRegistry] Found " + dtsFiles.size() + " .d.ts files across " + domainCounts.size() + " domains");
for (Map.Entry<String, Integer> entry : domainCounts.entrySet()) {
System.out.println("[JSTypeRegistry] Domain " + entry.getKey() + ": " + entry.getValue() + " files");
}
}
private static void scanDirectoryForModDts(File baseDir, String modId, List<DtsFileRef> dtsFiles) {
scanDirectoryForModDts(baseDir, baseDir, modId, dtsFiles);
}
private static void scanDirectoryForModDts(File baseDir, File directory, String modId, List<DtsFileRef> dtsFiles) {
File[] files = directory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
if (file.isDirectory()) {
scanDirectoryForModDts(baseDir, file, modId, dtsFiles);
continue;
}
if (!file.getName().endsWith(".d.ts")) {
continue;
}
String relativePath = baseDir.toURI().relativize(file.toURI()).getPath();
if (relativePath == null) {
continue;
}
String normalized = relativePath.replace('\\', '/');
Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(normalized);
if (matcher.matches()) {
String domain = matcher.group(1);
String apiPath = matcher.group(2);
dtsFiles.add(DtsFileRef.forFile(modId, domain, apiPath, file));
}
}
}
private static void scanJarForModDts(File jarFile, String modId, List<DtsFileRef> dtsFiles) {
try {
JarFile jar = new JarFile(jarFile);
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
Matcher matcher = MOD_DTS_PATH_PATTERN.matcher(entryName);
if (matcher.matches()) {
String domain = matcher.group(1);
String apiPath = matcher.group(2);
dtsFiles.add(DtsFileRef.forJar(modId, domain, apiPath, jarFile, entryName));
}
}
jar.close();
} catch (Exception e) {
System.err.println("[JSTypeRegistry] Error scanning mod jar for .d.ts files: " + e.getMessage());
}
}
static final class DtsFileRef {
private final String modId;
private final String domain;
private final String relativePath;
private final File file;
private final File jarFile;
private final String jarEntryName;
private DtsFileRef(String modId, String domain, String relativePath, File file, File jarFile, String jarEntryName) {
this.modId = modId;
this.domain = domain;
this.relativePath = relativePath;
this.file = file;
this.jarFile = jarFile;
this.jarEntryName = jarEntryName;
}
static DtsFileRef forFile(String modId, String domain, String relativePath, File file) {
return new DtsFileRef(modId, domain, relativePath, file, null, null);
}
static DtsFileRef forJar(String modId, String domain, String relativePath, File jarFile, String jarEntryName) {
return new DtsFileRef(modId, domain, relativePath, null, jarFile, jarEntryName);
}
InputStream openStream() throws IOException {
if (file != null) {
return new FileInputStream(file);
}
if (jarFile != null && jarEntryName != null) {
final JarFile jar = new JarFile(jarFile);
JarEntry entry = jar.getJarEntry(jarEntryName);
if (entry == null) {
jar.close();
return null;
}
InputStream is = jar.getInputStream(entry);
return new FilterInputStream(is) {
@Override
public void close() throws IOException {
super.close();
jar.close();
}
};
}
return null;
}
String getDomain() {
return domain;
}
String getRelativePath() {
return relativePath;
}
String getOrigin() {
return modId + ":" + domain + ":" + relativePath;
}
}
private static class DtsFileRefComparator implements Comparator<DtsFileRef> {
@Override
public int compare(DtsFileRef a, DtsFileRef b) {
int domainCompare = Integer.compare(getDomainRank(a.getDomain()), getDomainRank(b.getDomain()));
if (domainCompare != 0) {
return domainCompare;
}
int domainNameCompare = a.getDomain().compareTo(b.getDomain());
if (domainNameCompare != 0) {
return domainNameCompare;
}
int filePriorityCompare = Integer.compare(getFilePriority(a.getRelativePath()), getFilePriority(b.getRelativePath()));
if (filePriorityCompare != 0) {
return filePriorityCompare;
}
return a.getRelativePath().compareTo(b.getRelativePath());
}
private int getDomainRank(String domain) {
int idx = DOMAIN_PRIORITY.indexOf(domain);
return idx >= 0 ? idx : DOMAIN_PRIORITY.size();
}
private int getFilePriority(String relativePath) {
if (relativePath.endsWith("hooks.d.ts")) {
return 0;
}
if (relativePath.endsWith("index.d.ts")) {
return 1;
}
return 2;
}
}
}
@@ -0,0 +1,120 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
/**
* Represents a field/property in a TypeScript interface.
*/
public class JSFieldInfo {
private final String name;
private final String type; // Raw type string for display
private TypeInfo typeInfo; // Resolved TypeInfo
private final boolean readonly;
private boolean isStatic;
private JSDocInfo jsDocInfo;
private JSTypeInfo containingType; // The type that contains this field
public JSFieldInfo(String name, String type, boolean readonly) {
this.name = name;
this.type = type;
this.readonly = readonly;
}
public JSFieldInfo setJsDocInfo(JSDocInfo jsDocInfo) {
this.jsDocInfo = jsDocInfo;
return this;
}
public boolean isStatic() { return isStatic; }
public JSFieldInfo setStatic(boolean isStatic) {
this.isStatic = isStatic;
return this;
}
/**
* Set the containing type (the JSTypeInfo that owns this field).
*/
public void setContainingType(JSTypeInfo containingType) {
this.containingType = containingType;
}
public void setTypeInfo(TypeInfo typeInfo) {
this.typeInfo = typeInfo;
}
/**
* Get the resolved type, with fallback resolution using type parameters.
* @param contextType The TypeInfo context for resolving type parameters
* @return The resolved TypeInfo for the field type
*/
public TypeInfo getResolvedType(TypeInfo contextType) {
TypeResolver resolver = TypeResolver.getInstance();
// Cache only the raw resolved type; substitutions depend on the receiver context.
TypeInfo resolved = typeInfo != null ? typeInfo : resolver.resolveJSType(type);
if (typeInfo == null) {
typeInfo = resolved;
}
if (contextType != null) {
GenericContext ctx = GenericContext.forReceiver(contextType);
return ctx.substituteType(resolved, type, resolver);
}
return resolved;
}
// Getters
public String getName() { return name; }
public String getType() { return type; }
public TypeInfo getTypeInfo() { return typeInfo; }
public boolean isReadonly() { return readonly; }
public JSDocInfo getJsDocInfo() { return jsDocInfo; }
public JSTypeInfo getContainingType() { return containingType; }
/**
* Get documentation string (backward compatibility).
* Extracts description from JSDocInfo if available.
*/
public String getDocumentation() {
return jsDocInfo != null ? jsDocInfo.getDescription() : null;
}
/**
* Get display name - uses resolved TypeInfo display name if available,
* including generic type arguments like List<String>.
*/
public String getDisplayType() {
if (typeInfo != null && typeInfo.isResolved()) {
return typeInfo.getDisplayName();
}
return type;
}
/**
* Build hover info HTML for this field.
*/
public String buildHoverInfo() {
StringBuilder sb = new StringBuilder();
if (readonly) {
sb.append("<i>readonly</i> ");
}
sb.append("<b>").append(name).append("</b>: <i>").append(type).append("</i>");
if (jsDocInfo != null && jsDocInfo.getDescription() != null && !jsDocInfo.getDescription().isEmpty()) {
sb.append("<br><br>").append(jsDocInfo.getDescription());
}
return sb.toString();
}
@Override
public String toString() {
return (readonly ? "readonly " : "") + name + ": " + type;
}
}
@@ -0,0 +1,205 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
import java.util.*;
/**
* Represents a method signature parsed from .d.ts files.
*/
public class JSMethodInfo {
private final String name;
private final String returnType; // Raw type string for display, e.g., "number", "IEntity", "void"
private TypeInfo returnTypeInfo; // Resolved TypeInfo (set during Phase 2)
private final List<JSParameterInfo> parameters;
private boolean isStatic;
private JSDocInfo jsDocInfo;
private JSTypeInfo containingType; // The type that contains this method
public JSMethodInfo(String name, String returnType, List<JSParameterInfo> parameters) {
this.name = name;
this.returnType = returnType;
this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>();
}
public JSMethodInfo setJsDocInfo(JSDocInfo jsDocInfo) {
this.jsDocInfo = jsDocInfo;
return this;
}
public boolean isStatic() { return isStatic; }
public JSMethodInfo setStatic(boolean isStatic) {
this.isStatic = isStatic;
return this;
}
/**
* Set the resolved return type info (called during Phase 2 type resolution).
*/
public void setReturnTypeInfo(TypeInfo typeInfo) {
this.returnTypeInfo = typeInfo;
}
/**
* Set the containing type (the JSTypeInfo that owns this method).
*/
public void setContainingType(JSTypeInfo containingType) {
this.containingType = containingType;
// Also set for all parameters
for (JSParameterInfo param : parameters)
param.setContainingMethod(this);
}
/**
* Get the resolved return type, with type parameter substitution based on receiver context.
*
* For parameterized receivers like List<String>, this will substitute type variables:
* - If return type is "T" and receiver is List<String>, returns TypeInfo(String)
* - Handles nested generics like List<Map<K,V>> with proper substitution
*
* @param contextType The TypeInfo context for resolving type parameters (e.g., List<String> to resolve E → String)
* @return The resolved TypeInfo for the return type, with type variables substituted
*/
public TypeInfo getResolvedReturnType(TypeInfo contextType) {
TypeResolver resolver = TypeResolver.getInstance();
TypeInfo resolved = resolver.resolveJSType(returnType);
if (contextType != null) {
GenericContext ctx = GenericContext.forReceiver(contextType);
resolved = ctx.substituteType(resolved, returnType, resolver);
}
return resolved;
}
// Getters
public String getName() { return name; }
public String getReturnType() { return returnType; }
public TypeInfo getReturnTypeInfo() { return returnTypeInfo; }
public List<JSParameterInfo> getParameters() { return parameters; }
public JSDocInfo getJsDocInfo() { return jsDocInfo; }
public int getParameterCount() { return parameters.size(); }
public JSTypeInfo getContainingType() { return containingType; }
/**
* Get documentation string (backward compatibility).
* Extracts description from JSDocInfo if available.
*/
public String getDocumentation() {
return jsDocInfo != null ? jsDocInfo.getDescription() : null;
}
/**
* Get a formatted signature string for display.
*/
public String getSignature() {
StringBuilder sb = new StringBuilder();
sb.append(name).append("(");
for (int i = 0; i < parameters.size(); i++) {
if (i > 0) sb.append(", ");
JSParameterInfo param = parameters.get(i);
sb.append(param.getName()).append(": ").append(param.getType());
}
sb.append("): ").append(returnType);
return sb.toString();
}
/**
* Build hover info HTML for this method.
*/
public String buildHoverInfo() {
StringBuilder sb = new StringBuilder();
sb.append("<b>").append(name).append("</b>(");
for (int i = 0; i < parameters.size(); i++) {
if (i > 0) sb.append(", ");
JSParameterInfo param = parameters.get(i);
sb.append(param.getName()).append(": <i>").append(param.getType()).append("</i>");
}
sb.append("): <i>").append(returnType).append("</i>");
if (jsDocInfo != null && jsDocInfo.getDescription() != null && !jsDocInfo.getDescription().isEmpty()) {
sb.append("<br><br>").append(jsDocInfo.getDescription());
}
return sb.toString();
}
@Override
public String toString() {
return getSignature();
}
/**
* Represents a parameter in a method signature.
*/
public static class JSParameterInfo {
private final String name;
private final String type;
private TypeInfo typeInfo; // Resolved TypeInfo (set during Phase 2)
private JSMethodInfo containingMethod; // The type that contains the method with this parameter
private boolean isVarArg = false;
public JSParameterInfo(String name, String type) {
this.name = name;
this.type = type;
}
public void setVarArg(boolean isVarArg) { this.isVarArg = isVarArg; }
public boolean isVarArg() { return isVarArg; }
public void setContainingMethod(JSMethodInfo containingMethod) {
this.containingMethod = containingMethod;
}
public void setTypeInfo(TypeInfo typeInfo) {
this.typeInfo = typeInfo;
}
/**
* Get the resolved type, with type parameter substitution based on receiver context.
*
* For parameterized receivers like Consumer<IAction>, this will substitute type variables:
* - If param type is "T" and receiver is Consumer<IAction>, returns TypeInfo(IAction)
*
* @param contextType The TypeInfo context for resolving type parameters
* @return The resolved TypeInfo for the parameter type
*/
public TypeInfo getResolvedType(TypeInfo contextType) {
TypeResolver resolver = TypeResolver.getInstance();
TypeInfo resolved = resolver.resolveJSType(type);
if (contextType != null) {
GenericContext ctx = GenericContext.forReceiver(contextType);
resolved = ctx.substituteType(resolved, type, resolver);
}
return resolved;
}
public String getName() { return name; }
public String getType() { return type; }
public TypeInfo getTypeInfo() {return typeInfo;}
public JSMethodInfo getContainingMethod() {return containingMethod;}
/**
* Get display name - uses resolved TypeInfo display name if available,
* including generic type arguments like List<String>.
*/
public String getDisplayType() {
if (typeInfo != null && typeInfo.isResolved()) {
return typeInfo.getDisplayName();
}
return type;
}
@Override
public String toString() {
return name + ": " + type;
}
}
}
@@ -0,0 +1,554 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
import noppes.npcs.client.gui.util.script.interpreter.ScriptLine;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import scala.annotation.meta.field;
import java.util.*;
import java.util.regex.*;
/**
* Analyzes JavaScript scripts to produce syntax highlighting marks and type information.
* Uses the JSTypeRegistry to resolve types and validate member access.
*
* @deprecated This class is deprecated. All JavaScript analysis is now handled by the
* unified pipeline in {@link ScriptDocument}. JavaScript and Java scripts now use the
* same methods and data structures:
* <ul>
* <li>{@link ScriptDocument#parseMethodDeclarations()} - for both Java methods and JS functions</li>
* <li>{@link ScriptDocument#parseLocalVariables()} - for both Java and JS local variables</li>
* <li>{@link ScriptDocument#parseGlobalFields()} - for both Java and JS global variables</li>
* <li>{@link ScriptDocument#buildMarks(List)} - for mark building (both languages)</li>
* </ul>
* Type information is available via the unified data structures: {@code getMethods()},
* {@code getMethodLocals()}, and {@code getGlobalFields()}.
*/
@Deprecated
public class JSScriptAnalyzer {
private final ScriptDocument document;
private final String text;
private final JSTypeRegistry registry;
// Variable tracking: varName -> inferred type name
private final Map<String, String> variableTypes = new HashMap<>();
// Function parameter types: funcName -> paramName -> typeName
private final Map<String, Map<String, String>> functionParams = new HashMap<>();
// Excluded ranges (strings, comments)
private final List<int[]> excludedRanges = new ArrayList<>();
// Patterns
private static final Pattern STRING_PATTERN = Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*\"|'(?:[^'\\\\]|\\\\.)*'");
private static final Pattern COMMENT_PATTERN = Pattern.compile("//.*?$|/\\*.*?\\*/", Pattern.MULTILINE | Pattern.DOTALL);
private static final Pattern FUNCTION_PATTERN = Pattern.compile("function\\s+(\\w+)\\s*\\(([^)]*)\\)");
private static final Pattern VAR_DECL_PATTERN = Pattern.compile("(?:var|let|const)\\s+(\\w+)(?:\\s*=\\s*([^;]+))?");
private static final Pattern ASSIGNMENT_PATTERN = Pattern.compile("(\\w+)\\s*=\\s*([^;]+)");
private static final Pattern METHOD_CALL_PATTERN = Pattern.compile("(\\w+(?:\\.\\w+)*)\\s*\\(");
private static final Pattern MEMBER_ACCESS_PATTERN = Pattern.compile("(\\w+)(?:\\.(\\w+))+");
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b");
private static final Pattern JS_KEYWORD_PATTERN = Pattern.compile(
"\\b(function|var|let|const|if|else|for|while|do|switch|case|break|continue|return|" +
"try|catch|finally|throw|new|typeof|instanceof|in|of|this|null|undefined|true|false|" +
"class|extends|import|export|default|async|await|yield)\\b");
private static final Pattern NUMBER_PATTERN = Pattern.compile("\\b\\d+\\.?\\d*\\b");
public JSScriptAnalyzer(ScriptDocument document) {
this.document = document;
this.text = document.getText();
this.registry = JSTypeRegistry.getInstance();
// Ensure registry is initialized
if (!registry.isInitialized()) {
registry.initializeFromResources();
}
}
/**
* Analyze the script and produce marks.
*/
public List<ScriptLine.Mark> analyze() {
List<ScriptLine.Mark> marks = new ArrayList<>();
// First pass: find excluded regions
findExcludedRanges();
// Mark comments and strings
addPatternMarks(marks, COMMENT_PATTERN, TokenType.COMMENT);
addPatternMarks(marks, STRING_PATTERN, TokenType.STRING);
// Mark keywords
addPatternMarks(marks, JS_KEYWORD_PATTERN, TokenType.KEYWORD);
// Mark numbers
addPatternMarks(marks, NUMBER_PATTERN, TokenType.LITERAL);
// Parse functions and infer parameter types from hooks
parseFunctions(marks);
// Parse variable declarations
parseVariables(marks);
// Mark member accesses with type validation
markMemberAccesses(marks);
// Mark method calls
markMethodCalls(marks);
// Mark standalone identifiers (parameters and variables in function bodies)
markIdentifiers(marks);
return marks;
}
/**
* Find strings and comments to exclude from analysis.
*/
private void findExcludedRanges() {
Matcher m = STRING_PATTERN.matcher(text);
while (m.find()) {
excludedRanges.add(new int[]{m.start(), m.end()});
}
m = COMMENT_PATTERN.matcher(text);
while (m.find()) {
excludedRanges.add(new int[]{m.start(), m.end()});
}
}
private boolean isExcluded(int pos) {
for (int[] range : excludedRanges) {
if (pos >= range[0] && pos < range[1]) return true;
}
return false;
}
/**
* Parse function declarations and infer parameter types from hook signatures.
*/
private void parseFunctions(List<ScriptLine.Mark> marks) {
Matcher m = FUNCTION_PATTERN.matcher(text);
while (m.find()) {
if (isExcluded(m.start())) continue;
String funcName = m.group(1);
String params = m.group(2);
// Mark function name
int nameStart = m.start(1);
int nameEnd = m.end(1);
// Check if this is a known hook
if (registry.isHook(funcName)) {
// Create a unified MethodInfo for the hook
List<JSTypeRegistry.HookSignature> sigs = registry.getHookSignatures(funcName);
if (!sigs.isEmpty()) {
JSTypeRegistry.HookSignature sig = sigs.get(0);
// Create unified MethodInfo for hover info
TypeInfo paramTypeInfo = resolveJSType(sig.paramType);
List<FieldInfo> methodParams = new ArrayList<>();
methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo));
MethodInfo hookMethod = MethodInfo.declaration(
funcName, null, TypeInfo.fromPrimitive("void"), methodParams,
nameStart, nameStart, nameStart, -1, -1, 0, sig.doc
);
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, hookMethod));
// Infer parameter types from hook
if (!params.isEmpty()) {
// Parse parameter names from function
String[] paramNames = params.split(",");
if (paramNames.length > 0) {
String paramName = paramNames[0].trim();
String paramType = sig.paramType;
// Store in function params and variable types
Map<String, String> funcParamMap = new HashMap<>();
funcParamMap.put(paramName, paramType);
functionParams.put(funcName, funcParamMap);
variableTypes.put(paramName, paramType);
// Mark the parameter with unified FieldInfo
int paramStart = m.start(2) + params.indexOf(paramName);
int paramEnd = paramStart + paramName.length();
FieldInfo paramFieldInfo = FieldInfo.parameter(
paramName, paramTypeInfo, paramStart, hookMethod
);
marks.add(new ScriptLine.Mark(paramStart, paramEnd, TokenType.PARAMETER, paramFieldInfo));
}
}
} else {
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName));
}
} else {
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_DECL, funcName));
}
}
}
/**
* Parse variable declarations and infer types.
*/
private void parseVariables(List<ScriptLine.Mark> marks) {
Matcher m = VAR_DECL_PATTERN.matcher(text);
while (m.find()) {
if (isExcluded(m.start())) continue;
String varName = m.group(1);
String initializer = m.group(2);
// Infer type from initializer
String inferredType = null;
if (initializer != null) {
inferredType = inferTypeFromExpression(initializer.trim());
}
if (inferredType != null) {
variableTypes.put(varName, inferredType);
}
// Mark variable declaration with unified FieldInfo
int varStart = m.start(1);
int varEnd = m.end(1);
TypeInfo typeInfo = resolveJSType(inferredType != null ? inferredType : "any");
FieldInfo varFieldInfo = FieldInfo.localField(varName, typeInfo, varStart, null);
marks.add(new ScriptLine.Mark(varStart, varEnd, TokenType.LOCAL_FIELD, varFieldInfo));
}
}
/**
* Infer type from an expression.
*/
private String inferTypeFromExpression(String expr) {
if (expr == null || expr.isEmpty()) return null;
// String literal
if (expr.startsWith("\"") || expr.startsWith("'")) {
return "string";
}
// Number literal
if (expr.matches("\\d+\\.?\\d*")) {
return "number";
}
// Boolean literal
if (expr.equals("true") || expr.equals("false")) {
return "boolean";
}
// null/undefined
if (expr.equals("null")) return "null";
if (expr.equals("undefined")) return "undefined";
// Array literal
if (expr.startsWith("[")) {
return "any[]";
}
// Object literal
if (expr.startsWith("{")) {
return "object";
}
// Method call: something.method() - infer from method return type
if (expr.contains(".") && expr.contains("(")) {
return inferTypeFromMethodCall(expr);
}
// Variable reference
if (variableTypes.containsKey(expr)) {
return variableTypes.get(expr);
}
return null;
}
/**
* Infer type from a method call chain.
*/
private String inferTypeFromMethodCall(String expr) {
// Remove trailing parentheses and args for analysis
int parenIndex = expr.indexOf('(');
if (parenIndex > 0) {
expr = expr.substring(0, parenIndex);
}
String[] parts = expr.split("\\.");
if (parts.length < 2) return null;
// Start with the receiver type
String currentType = variableTypes.get(parts[0]);
if (currentType == null) return null;
// Walk the chain
for (int i = 1; i < parts.length; i++) {
JSTypeInfo typeInfo = registry.getType(currentType);
if (typeInfo == null) return null;
String member = parts[i];
// Check if it's a method
JSMethodInfo method = typeInfo.getMethod(member);
if (method != null) {
currentType = method.getReturnType();
continue;
}
// Check if it's a field
JSFieldInfo f = typeInfo.getField(member);
if (f != null) {
currentType = f.getType();
continue;
}
return null; // Unknown member
}
return currentType;
}
/**
* Mark member accesses (x.y.z) with type validation.
*/
private void markMemberAccesses(List<ScriptLine.Mark> marks) {
Matcher m = MEMBER_ACCESS_PATTERN.matcher(text);
while (m.find()) {
if (isExcluded(m.start())) continue;
String fullAccess = m.group(0);
String[] parts = fullAccess.split("\\.");
if (parts.length < 2) continue;
// Get receiver type
String receiverName = parts[0];
String currentType = variableTypes.get(receiverName);
int pos = m.start();
// Mark receiver with unified FieldInfo
if (currentType != null) {
TypeInfo unifiedType = resolveJSType(currentType);
FieldInfo receiverField = FieldInfo.localField(receiverName, unifiedType, pos, null);
marks.add(new ScriptLine.Mark(pos, pos + receiverName.length(), TokenType.LOCAL_FIELD, receiverField));
}
pos += receiverName.length() + 1; // +1 for the dot
// Walk the chain and mark each member
for (int i = 1; i < parts.length; i++) {
String member = parts[i];
int memberStart = pos;
int memberEnd = pos + member.length();
if (currentType != null) {
JSTypeInfo jsTypeInfo = registry.getType(currentType);
if (jsTypeInfo != null) {
TypeInfo unifiedContainingType = TypeInfo.fromJSTypeInfo(jsTypeInfo);
// Check method first
JSMethodInfo jsMethod = jsTypeInfo.getMethod(member);
if (jsMethod != null) {
// Convert to unified MethodInfo
MethodInfo unifiedMethod = MethodInfo.fromJSMethod(jsMethod, unifiedContainingType);
marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.METHOD_CALL, unifiedMethod));
currentType = jsMethod.getReturnType();
} else {
// Check field
JSFieldInfo jsField = jsTypeInfo.getField(member);
if (jsField != null) {
// Convert to unified FieldInfo
FieldInfo unifiedField = FieldInfo.fromJSField(jsField, unifiedContainingType);
marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.GLOBAL_FIELD, unifiedField));
currentType = jsField.getType();
} else {
// Unknown member - mark as undefined
marks.add(new ScriptLine.Mark(memberStart, memberEnd, TokenType.UNDEFINED_VAR,
"Unknown member '" + member + "' on type " + jsTypeInfo.getFullName()));
currentType = null;
}
}
} else {
currentType = null;
}
}
pos = memberEnd + 1; // +1 for the next dot
}
}
}
/**
* Mark method calls.
*/
private void markMethodCalls(List<ScriptLine.Mark> marks) {
Matcher m = METHOD_CALL_PATTERN.matcher(text);
while (m.find()) {
if (isExcluded(m.start())) continue;
String callExpr = m.group(1);
if (!callExpr.contains(".")) {
// Simple function call
int nameStart = m.start(1);
int nameEnd = m.end(1);
if (registry.isHook(callExpr)) {
// It's a known hook being called - create unified MethodInfo
List<JSTypeRegistry.HookSignature> sigs = registry.getHookSignatures(callExpr);
if (!sigs.isEmpty()) {
JSTypeRegistry.HookSignature sig = sigs.get(0);
TypeInfo paramTypeInfo = resolveJSType(sig.paramType);
List<FieldInfo> methodParams = new ArrayList<>();
methodParams.add(FieldInfo.reflectionParam(sig.paramName, paramTypeInfo));
MethodInfo hookMethod = MethodInfo.declaration(
callExpr, null, TypeInfo.fromPrimitive("void"), methodParams,
nameStart, nameStart, nameStart, -1, -1, 0, sig.doc
);
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.METHOD_CALL, hookMethod));
}
}
}
// Chained calls are handled in markMemberAccesses
}
}
/**
* Add pattern-based marks.
*/
private void addPatternMarks(List<ScriptLine.Mark> marks, Pattern pattern, TokenType type) {
Matcher m = pattern.matcher(text);
while (m.find()) {
marks.add(new ScriptLine.Mark(m.start(), m.end(), type));
}
}
/**
* Get the inferred type of a variable at a position.
*/
public String getVariableType(String varName) {
return variableTypes.get(varName);
}
/**
* Get all inferred variable types.
*/
public Map<String, String> getVariableTypes() {
return new HashMap<>(variableTypes);
}
/**
* Resolves a JavaScript type name to a unified TypeInfo.
* Handles primitives, mapped types, and custom types from the registry.
*/
private TypeInfo resolveJSType(String jsTypeName) {
if (jsTypeName == null || jsTypeName.isEmpty() || "void".equals(jsTypeName)) {
return TypeInfo.fromPrimitive("void");
}
// Handle JS primitives
switch (jsTypeName) {
case "string":
return TypeInfo.fromClass(String.class);
case "number":
return TypeInfo.fromClass(double.class);
case "boolean":
return TypeInfo.fromClass(boolean.class);
case "any":
return TypeInfo.fromClass(Object.class);
case "void":
return TypeInfo.fromPrimitive("void");
}
// Handle array types
if (jsTypeName.endsWith("[]")) {
String elementType = jsTypeName.substring(0, jsTypeName.length() - 2);
TypeInfo elementTypeInfo = resolveJSType(elementType);
return TypeInfo.arrayOf(elementTypeInfo);
}
// Try to resolve from the JS type registry
if (registry != null) {
JSTypeInfo jsTypeInfo = registry.getType(jsTypeName);
if (jsTypeInfo != null) {
return TypeInfo.fromJSTypeInfo(jsTypeInfo);
}
}
// Fallback: unresolved type
return TypeInfo.unresolved(jsTypeName, jsTypeName);
}
/**
* Mark all identifiers in the script - variables and parameters.
* This ensures consistent coloring for parameters throughout function bodies.
*/
private void markIdentifiers(List<ScriptLine.Mark> marks) {
// Build set of positions already marked
Set<Integer> markedPositions = new HashSet<>();
for (ScriptLine.Mark mark : marks) {
for (int i = mark.start; i < mark.end; i++) {
markedPositions.add(i);
}
}
// Find all identifiers
Matcher matcher = IDENTIFIER_PATTERN.matcher(text);
while (matcher.find()) {
if (isExcluded(matcher.start())) {
continue;
}
int start = matcher.start();
int end = matcher.end();
// Skip if already marked
boolean alreadyMarked = false;
for (int i = start; i < end; i++) {
if (markedPositions.contains(i)) {
alreadyMarked = true;
break;
}
}
if (alreadyMarked) {
continue;
}
String identifier = matcher.group();
// Check if it's a parameter
boolean isParameter = false;
for (Map<String, String> params : functionParams.values()) {
if (params.containsKey(identifier)) {
isParameter = true;
break;
}
}
if (isParameter) {
marks.add(new ScriptLine.Mark(start, end, TokenType.PARAMETER));
// Mark this position as used
for (int i = start; i < end; i++) {
markedPositions.add(i);
}
} else if (variableTypes.containsKey(identifier)) {
// It's a local variable
marks.add(new ScriptLine.Mark(start, end, TokenType.LOCAL_FIELD));
// Mark this position as used
for (int i = start; i < end; i++) {
markedPositions.add(i);
}
}
}
}
}
@@ -0,0 +1,311 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocParamTag;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocReturnTag;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTypeTag;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
import java.util.*;
/**
* Represents a TypeScript interface/type parsed from .d.ts files.
* This is the JS equivalent of TypeInfo for Java types.
*/
public class JSTypeInfo {
private final String simpleName; // e.g., "InteractEvent"
private final String fullName; // e.g., "IPlayerEvent.InteractEvent"
private final String namespace; // e.g., "IPlayerEvent" (parent namespace, null for top-level)
// Java fully-qualified name (e.g., "noppes.npcs.api.entity.IPlayer")
private String javaFqn;
// Type parameters (generics)
private final List<TypeParamInfo> typeParams = new ArrayList<>();
// Members
private final Map<String, JSMethodInfo> methods = new LinkedHashMap<>();
private final Map<String, JSFieldInfo> fields = new LinkedHashMap<>();
// Inheritance
private String extendsType; // The type this interface extends
private JSTypeInfo resolvedParent; // Resolved parent type (after registry is built)
// Inner types (for namespaces)
private final Map<String, JSTypeInfo> innerTypes = new LinkedHashMap<>();
private JSTypeInfo parentType; // The containing type (for inner types)
// Documentation
private JSDocInfo jsDocInfo;
public JSTypeInfo(String simpleName, String namespace) {
this.simpleName = simpleName;
this.namespace = namespace;
this.fullName = namespace != null ? namespace + "." + simpleName : simpleName;
}
// Builder methods
public JSTypeInfo setExtends(String extendsType) {
this.extendsType = extendsType;
return this;
}
public JSTypeInfo setJsDocInfo(JSDocInfo jsDocInfo) {
this.jsDocInfo = jsDocInfo;
return this;
}
public JSTypeInfo setJavaFqn(String javaFqn) {
this.javaFqn = javaFqn;
return this;
}
public void addMethod(JSMethodInfo method) {
method.setContainingType(this);
// Handle overloads - store with index if name already exists
String key = method.getName();
if (methods.containsKey(key)) {
// Find next available key for overload
int index = 1;
while (methods.containsKey(key + "$" + index)) {
index++;
}
methods.put(key + "$" + index, method);
} else {
methods.put(key, method);
}
}
public void addField(JSFieldInfo field) {
field.setContainingType(this);
fields.put(field.getName(), field);
}
public void addInnerType(JSTypeInfo inner) {
inner.parentType = this;
innerTypes.put(inner.getSimpleName(), inner);
}
public void setResolvedParent(JSTypeInfo parent) {
this.resolvedParent = parent;
}
// Getters
public String getSimpleName() { return simpleName; }
public String getFullName() { return fullName; }
public String getNamespace() { return namespace; }
public String getJavaFqn() { return javaFqn; }
public String getExtendsType() { return extendsType; }
public JSTypeInfo getResolvedParent() { return resolvedParent; }
public JSDocInfo getJsDocInfo() { return jsDocInfo; }
public JSTypeInfo getParentType() { return parentType; }
public List<TypeParamInfo> getTypeParams() { return typeParams; }
public Map<String, JSMethodInfo> getMethods() { return methods; }
public Map<String, JSFieldInfo> getFields() { return fields; }
public Map<String, JSTypeInfo> getInnerTypes() { return innerTypes; }
/**
* Get the type parameter info for a given parameter name (e.g., "T").
* @return TypeParamInfo or null if not found
*/
public TypeParamInfo getTypeParam(String name) {
for (TypeParamInfo param : typeParams) {
if (param.getName().equals(name)) {
return param;
}
}
return null;
}
public void addTypeParam(TypeParamInfo param) {
typeParams.add(param);
}
/**
* Resolve all type parameters for this type.
* Called during Phase 2 after all types are loaded into the registry.
*/
public void resolveTypeParameters() {
for (TypeParamInfo param : typeParams) {
param.resolveBoundType();
}
}
/**
* Resolve all member types (return types, field types, parameter types).
* Called during Phase 2 after all types are loaded into the registry.
* This resolves types like "Java.java.io.File" to proper TypeInfo objects.
*/
public void resolveMemberTypes() {
TypeResolver resolver = TypeResolver.getInstance();
// Resolve method return types and parameter types
for (JSMethodInfo method : methods.values()) {
TypeInfo returnTypeInfo = resolver.resolveJSType(method.getReturnType());
method.setReturnTypeInfo(returnTypeInfo);
// Resolve parameter types
for (JSMethodInfo.JSParameterInfo param : method.getParameters()) {
TypeInfo paramTypeInfo = resolver.resolveJSType(param.getType());
param.setTypeInfo(paramTypeInfo);
}
}
// Resolve field types
for (JSFieldInfo field : fields.values()) {
TypeInfo fieldTypeInfo = resolver.resolveJSType(field.getType());
field.setTypeInfo(fieldTypeInfo);
}
}
public void resolveJSDocTypes() {
TypeResolver resolver = TypeResolver.getInstance();
if (jsDocInfo != null) {
resolveJSDocInfoTypes(jsDocInfo, resolver);
}
for (JSMethodInfo method : methods.values()) {
JSDocInfo methodDoc = method.getJsDocInfo();
if (methodDoc != null) {
resolveJSDocInfoTypes(methodDoc, resolver);
}
}
for (JSFieldInfo field : fields.values()) {
JSDocInfo fieldDoc = field.getJsDocInfo();
if (fieldDoc != null) {
resolveJSDocInfoTypes(fieldDoc, resolver);
}
}
}
private void resolveJSDocInfoTypes(JSDocInfo jsDoc, TypeResolver resolver) {
JSDocTypeTag typeTag = jsDoc.getTypeTag();
if (typeTag != null && typeTag.hasType() && typeTag.getTypeInfo() == null) {
TypeInfo resolved = resolver.resolveJSType(typeTag.getTypeName());
typeTag.setType(typeTag.getTypeName(), resolved, typeTag.getTypeStart(), typeTag.getTypeEnd());
}
for (JSDocParamTag paramTag : jsDoc.getParamTags()) {
if (paramTag.hasType() && paramTag.getTypeInfo() == null) {
TypeInfo resolved = resolver.resolveJSType(paramTag.getTypeName());
paramTag.setType(paramTag.getTypeName(), resolved, paramTag.getTypeStart(), paramTag.getTypeEnd());
}
}
JSDocReturnTag returnTag = jsDoc.getReturnTag();
if (returnTag != null && returnTag.hasType() && returnTag.getTypeInfo() == null) {
TypeInfo resolved = resolver.resolveJSType(returnTag.getTypeName());
returnTag.setType(returnTag.getTypeName(), resolved, returnTag.getTypeStart(), returnTag.getTypeEnd());
}
}
/**
* Resolves a type parameter to its bound TypeInfo.
* For example, if this type has "T extends EntityPlayerMP", resolveTypeParam("T") returns the TypeInfo for EntityPlayerMP.
* If no type parameter is found with that name, returns null.
*/
public TypeInfo resolveTypeParamToTypeInfo(String typeName) {
TypeParamInfo param = getTypeParam(typeName);
if (param != null) {
return param.getBoundTypeInfo();
}
return null;
}
/**
* Resolves a type parameter to its bound type name (for backward compatibility).
* @deprecated Use resolveTypeParamToTypeInfo instead
*/
@Deprecated
public String resolveTypeParam(String typeName) {
TypeParamInfo param = getTypeParam(typeName);
if (param != null && param.getBoundTypeInfo() != null) {
return param.getBoundTypeInfo().getSimpleName();
}
return typeName;
}
/**
* Get a method by name, including inherited methods.
*/
public JSMethodInfo getMethod(String name) {
JSMethodInfo method = methods.get(name);
if (method != null) return method;
// Check parent
if (resolvedParent != null) {
return resolvedParent.getMethod(name);
}
return null;
}
/**
* Get all methods with a given name (for overloads).
*/
public List<JSMethodInfo> getMethodOverloads(String name) {
List<JSMethodInfo> overloads = new ArrayList<>();
// Get from this type
if (methods.containsKey(name)) {
overloads.add(methods.get(name));
}
// Get numbered overloads
int index = 1;
while (methods.containsKey(name + "$" + index)) {
overloads.add(methods.get(name + "$" + index));
index++;
}
// Get from parent
if (resolvedParent != null) {
overloads.addAll(resolvedParent.getMethodOverloads(name));
}
return overloads;
}
/**
* Check if this type has a method (including inherited).
*/
public boolean hasMethod(String name) {
return getMethod(name) != null;
}
/**
* Get a field by name, including inherited fields.
*/
public JSFieldInfo getField(String name) {
JSFieldInfo field = fields.get(name);
if (field != null) return field;
// Check parent
if (resolvedParent != null) {
return resolvedParent.getField(name);
}
return null;
}
/**
* Check if this type has a field (including inherited).
*/
public boolean hasField(String name) {
return getField(name) != null;
}
/**
* Get an inner type by name.
*/
public JSTypeInfo getInnerType(String name) {
return innerTypes.get(name);
}
@Override
public String toString() {
return "JSTypeInfo{" + fullName + "}";
}
}
@@ -0,0 +1,68 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
/**
* Represents a generic type parameter like "T extends Entity".
* Stores the parameter name and both the string representation (for deferred resolution)
* and the resolved TypeInfo for the bound.
*/
public class TypeParamInfo {
private final String name; // e.g., "T"
private final String boundType; // Simple name like "EntityPlayerMP" (may be null)
private final String fullBoundType; // Full name like "net.minecraft.entity.player.EntityPlayerMP" (may be null)
private TypeInfo boundTypeInfo; // Resolved TypeInfo for the bound (resolved in Phase 2)
/**
* Constructor for parse-time (Phase 1) - stores string names only.
*/
public TypeParamInfo(String name, String boundType, String fullBoundType) {
this.name = name;
this.boundType = boundType;
this.fullBoundType = fullBoundType;
this.boundTypeInfo = null; // Will be resolved later
}
public String getName() { return name; }
public TypeInfo getBoundTypeInfo() {
return boundTypeInfo;
}
/**
* Resolve the bound type using TypeResolver.
* Called during Phase 2 after all types are loaded.
*/
public void resolveBoundType() {
if (boundTypeInfo != null)
return; // Already resolved
if (fullBoundType != null && !fullBoundType.isEmpty()) {
// Try to load the Java class using the full name
boundTypeInfo = TypeResolver.getInstance().resolveFullName(fullBoundType);
} else if (boundType != null && !boundType.isEmpty()) {
// Try to resolve using the simple name
boundTypeInfo = TypeResolver.getInstance().resolveJSType(boundType);
}
}
/**
* Get display name for the bound type, including generic arguments.
*/
public String getBoundTypeName() {
if (boundTypeInfo == null)
return null;
return boundTypeInfo.getDisplayName();
}
@Override
public String toString() {
if (boundTypeInfo != null) {
return name + " extends " + boundTypeInfo.getDisplayName();
} else if (boundType != null || fullBoundType != null) {
return name + " extends " + (boundType != null ? boundType : fullBoundType);
}
return name;
}
}
@@ -0,0 +1,728 @@
package noppes.npcs.client.gui.util.script.interpreter.js_parser;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocTag;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeStringNormalizer;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import java.util.zip.*;
/**
* Parses TypeScript definition files (.d.ts) to extract type information.
* Can read from individual files, directories, or .vsix archives.
*/
public class TypeScriptDefinitionParser {
// Patterns for parsing .d.ts content
// Updated to capture generic type parameters like: export interface IEntity<T extends Entity /* net.minecraft.entity.Entity */>
private static final Pattern INTERFACE_PATTERN = Pattern.compile(
"export\\s+interface\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{");
// Pattern for nested interfaces without export keyword
private static final Pattern NESTED_INTERFACE_PATTERN = Pattern.compile(
"(?<!export\\s)\\binterface\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{");
// Similar pattern for classes
private static final Pattern CLASS_PATTERN = Pattern.compile(
"export\\s+class\\s+(\\w+)(?:<([^>]*)>)?(?:\\s+extends\\s+([^{]+?))?\\s*\\{");
// Pattern to parse individual type parameters like: T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP */
private static final Pattern TYPE_PARAM_PATTERN = Pattern.compile(
"(\\w+)(?:\\s+extends\\s+(\\w+)(?:\\s*/\\*\\s*([\\w.]+)\\s*\\*/)?)?");
// Match both "export namespace" and plain "namespace" (for declare global blocks)
private static final Pattern NAMESPACE_PATTERN = Pattern.compile(
"(?:export\\s+)?namespace\\s+(\\w+)\\s*\\{");
// Make semicolon optional for type aliases (TypeScript doesn't require them)
private static final Pattern TYPE_ALIAS_PATTERN = Pattern.compile(
"export\\s+type\\s+(\\w+)\\s*=\\s*([^;\\n]+);?");
private static final Pattern METHOD_PATTERN = Pattern.compile(
"(?m)^\\s*(static\\s+)?(\\w+)\\??\\s*\\((.*)\\)\\s*:\\s*([^;]+);", Pattern.MULTILINE);
private static final Pattern FIELD_PATTERN = Pattern.compile(
"^\\s*(static\\s+)?(readonly\\s+)?(\\w+)\\??\\s*:\\s*([^;]+);", Pattern.MULTILINE);
private static final Pattern GLOBAL_FUNCTION_PATTERN = Pattern.compile(
"function\\s+(\\w+)\\s*\\((.*)\\)\\s*:\\s*([^;]+);");
private static final Pattern GLOBAL_TYPE_ALIAS_PATTERN = Pattern.compile(
"type\\s+(\\w+)\\s*=\\s*import\\(['\"]([^'\"]+)['\"]\\)\\.([\\w.]+);");
// Pattern for context-namespaced hooks like: declare namespace INpcEvent { ... }
// Matches any "declare namespace <Name> {" where Name starts with I and contains Event,
// or any other namespace pattern used for hooks
private static final Pattern HOOKS_NAMESPACE_PATTERN = Pattern.compile(
"declare\\s+namespace\\s+(I\\w*Event|\\w+)\\s*\\{", Pattern.MULTILINE);
private final JSTypeRegistry registry;
public TypeScriptDefinitionParser(JSTypeRegistry registry) {
this.registry = registry;
}
/**
* Parse all .d.ts files from a VSIX archive.
*/
public void parseVsixArchive(File vsixFile) throws IOException {
try (ZipFile zip = new ZipFile(vsixFile)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.getName().endsWith(".d.ts")) {
try (InputStream is = zip.getInputStream(entry);
BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
String content = readFully(reader);
String fileName = entry.getName();
parseDefinitionFile(content, fileName);
}
}
}
}
}
/**
* Parse all .d.ts files from a directory recursively.
*/
public void parseDirectory(File directory) throws IOException {
if (!directory.isDirectory()) {
throw new IllegalArgumentException("Not a directory: " + directory);
}
parseDirectoryRecursive(directory);
}
private void parseDirectoryRecursive(File dir) throws IOException {
File[] files = dir.listFiles();
if (files == null) return;
for (File file : files) {
if (file.isDirectory()) {
parseDirectoryRecursive(file);
} else if (file.getName().endsWith(".d.ts")) {
parseFile(file);
}
}
}
/**
* Parse a single .d.ts file.
*/
public void parseFile(File file) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String content = readFully(reader);
parseDefinitionFile(content, file.getName());
}
}
/**
* Parse a .d.ts file content.
*/
public void parseDefinitionFile(String content, String fileName) {
// Special handling for hooks.d.ts - extract function signatures
if (fileName.contains("hooks.d.ts")) {
// Commented out as hooks are now parsed through ScriptHookController which is the universal source of truth for registered hooks.
// parseHooksFile(content);
return;
}
// Special handling for index.d.ts - extract global type aliases
if (fileName.contains("index.d.ts")) {
parseIndexFile(content);
return;
}
// Parse regular interface files
String packageName = derivePackageName(fileName);
parseInterfaceFile(content, null, packageName);
}
/**
* Parse hooks.d.ts to extract function signatures for JS hooks.
*
* Hooks are organized by their parent event interface, with the interface name
* used directly as the namespace. Any mod can register its own event interfaces
* and they will be automatically parsed:
*
* declare namespace INpcEvent {
* function interact(event: INpcEvent.InteractEvent): void;
* function init(event: INpcEvent.InitEvent): void;
* }
*
* declare namespace IPlayerEvent {
* function interact(event: IPlayerEvent.InteractEvent): void;
* }
*
* declare namespace IDBCEvent {
* function customHook(event: IDBCEvent.CustomEvent): void;
* }
*
* The namespace name is stored as a string, allowing dynamic registration
* without requiring enum modifications.
*/
private void parseHooksFile(String content) {
// Parse namespaced hooks (e.g., declare namespace INpcEvent { ... })
// The namespace name is used directly - any event interface can register hooks
Matcher namespaceMatcher = HOOKS_NAMESPACE_PATTERN.matcher(content);
while (namespaceMatcher.find()) {
String namespace = namespaceMatcher.group(1); // e.g., "INpcEvent", "IPlayerEvent", "IDBCEvent"
// Find the body of this namespace
int bodyStart = namespaceMatcher.end();
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
if (bodyEnd > bodyStart) {
String namespaceBody = content.substring(bodyStart, bodyEnd);
// Parse functions within this namespace
Matcher funcMatcher = GLOBAL_FUNCTION_PATTERN.matcher(namespaceBody);
while (funcMatcher.find()) {
String funcName = funcMatcher.group(1);
String params = funcMatcher.group(2);
// Parse parameter - format is "paramName: TypeName"
if (!params.isEmpty()) {
String[] parts = params.split(":\\s*", 2);
if (parts.length == 2) {
String paramName = parts[0].trim();
String paramType = parts[1].trim();
// Register hook with the namespace string directly
registry.registerHook(namespace, funcName, paramName, paramType);
}
}
}
}
}
}
/**
* Parse index.d.ts to extract global type aliases.
*/
private void parseIndexFile(String content) {
Matcher m = GLOBAL_TYPE_ALIAS_PATTERN.matcher(content);
while (m.find()) {
String aliasName = m.group(1);
String importPath = m.group(2);
String typeName = m.group(3);
registry.registerTypeAlias(aliasName, typeName);
}
}
/**
* Parse interface and class definitions from content.
*/
private void parseInterfaceFile(String content, String parentNamespace, String packageName) {
// IMPORTANT:
// Many generated .d.ts files declare nested exported interfaces inside `namespace X { ... }` blocks.
// If we scan `content` directly for `export interface` / `export class`, we'll accidentally treat
// those nested exports as top-level for the current `parentNamespace`.
// That pollutes the registry with incorrectly-scoped type names (e.g. "DamagedEvent" instead of
// "INpcEvent.DamagedEvent"), and later registry merges (by @javaFqn) can prevent the correctly
// namespaced key from ever being registered.
//
// To avoid this, we strip namespace blocks out of the scan input and then parse namespaces
// explicitly via the NAMESPACE_PATTERN recursion further below.
String scanContent = stripNamespaceBlocks(content);
// Find exported interfaces
Matcher interfaceMatcher = INTERFACE_PATTERN.matcher(scanContent);
while (interfaceMatcher.find()) {
String interfaceName = interfaceMatcher.group(1);
String typeParamsStr = interfaceMatcher.group(2); // e.g., "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP */"
String extendsClause = interfaceMatcher.group(3);
JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace);
JSDocInfo jsDoc = extractJSDocBefore(content, interfaceMatcher.start());
if (jsDoc != null) {
typeInfo.setJsDocInfo(jsDoc);
}
String javaFqn = findJavaFqn(jsDoc, packageName, typeInfo.getFullName());
if (javaFqn != null && !javaFqn.isEmpty()) {
typeInfo.setJavaFqn(javaFqn);
}
// Parse type parameters
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
parseTypeParameters(typeParamsStr, typeInfo);
}
if (extendsClause != null) {
// Handle multiple extends (e.g., "IEntityLivingBase<T>, IAnimatable")
// Take the first one, stripping generics
String extendsType = extendsClause.trim();
// Remove generic parameters like <T>
extendsType = extendsType.replaceAll("<[^>]*>", "");
// If multiple types (comma-separated), take the first one
if (extendsType.contains(",")) {
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
}
// Clean up import() syntax if present
extendsType = cleanType(extendsType);
typeInfo.setExtends(extendsType);
}
// Find the body of this interface
int bodyStart = interfaceMatcher.end();
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
if (bodyEnd > bodyStart) {
String body = content.substring(bodyStart, bodyEnd);
parseInterfaceBody(body, typeInfo);
// Parse nested interfaces and type aliases within this interface body
String fullNamespace = parentNamespace != null ?
parentNamespace + "." + interfaceName : interfaceName;
parseNestedTypes(body, fullNamespace, packageName);
}
registry.registerType(typeInfo);
}
// Find nested interfaces (without export keyword)
Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(scanContent);
while (nestedInterfaceMatcher.find()) {
String interfaceName = nestedInterfaceMatcher.group(1);
String typeParamsStr = nestedInterfaceMatcher.group(2);
String extendsClause = nestedInterfaceMatcher.group(3);
JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, parentNamespace);
JSDocInfo jsDoc = extractJSDocBefore(content, nestedInterfaceMatcher.start());
if (jsDoc != null) {
typeInfo.setJsDocInfo(jsDoc);
}
// Parse type parameters
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
parseTypeParameters(typeParamsStr, typeInfo);
}
if (extendsClause != null) {
String extendsType = extendsClause.trim();
extendsType = extendsType.replaceAll("<[^>]*>", "");
if (extendsType.contains(",")) {
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
}
extendsType = cleanType(extendsType);
typeInfo.setExtends(extendsType);
}
// Find the body of this nested interface
int bodyStart = nestedInterfaceMatcher.end();
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
if (bodyEnd > bodyStart) {
String body = content.substring(bodyStart, bodyEnd);
parseInterfaceBody(body, typeInfo);
}
registry.registerType(typeInfo);
}
// Find classes (same logic as interfaces)
Matcher classMatcher = CLASS_PATTERN.matcher(scanContent);
while (classMatcher.find()) {
String className = classMatcher.group(1);
String typeParamsStr = classMatcher.group(2);
String extendsClause = classMatcher.group(3);
JSTypeInfo typeInfo = new JSTypeInfo(className, parentNamespace);
JSDocInfo jsDoc = extractJSDocBefore(content, classMatcher.start());
if (jsDoc != null) {
typeInfo.setJsDocInfo(jsDoc);
}
// Parse type parameters
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
parseTypeParameters(typeParamsStr, typeInfo);
}
if (extendsClause != null) {
String extendsType = extendsClause.trim();
extendsType = extendsType.replaceAll("<[^>]*>", "");
if (extendsType.contains(",")) {
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
}
extendsType = cleanType(extendsType);
typeInfo.setExtends(extendsType);
}
// Find the body of this class
int bodyStart = classMatcher.end();
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
if (bodyEnd > bodyStart) {
String body = content.substring(bodyStart, bodyEnd);
parseInterfaceBody(body, typeInfo);
}
registry.registerType(typeInfo);
}
// Find namespaces (which contain inner types)
Matcher namespaceMatcher = NAMESPACE_PATTERN.matcher(content);
while (namespaceMatcher.find()) {
String namespaceName = namespaceMatcher.group(1);
// Find the body of this namespace
int bodyStart = namespaceMatcher.end();
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
if (bodyEnd > bodyStart) {
String body = content.substring(bodyStart, bodyEnd);
// Parse inner types with namespace prefix
// Namespaces contain exported types, so use parseInterfaceFile
String fullNamespace = parentNamespace != null ?
parentNamespace + "." + namespaceName : namespaceName;
parseInterfaceFile(body, fullNamespace, packageName);
// Don't call parseNestedTypes here - namespace members are all exported
// and will be caught by parseInterfaceFile's INTERFACE_PATTERN
}
}
// Handle top-level type aliases
if (parentNamespace == null) {
parseTypeAliases(content, null);
}
}
/**
* Strip namespace blocks from a file-level scan to avoid incorrectly capturing nested exports.
*
* This replaces the entire `namespace X { ... }` span (including braces) with whitespace so that
* match indices remain stable when we later slice `content` for bodies.
*/
private String stripNamespaceBlocks(String content) {
if (content == null || content.isEmpty()) {
return content;
}
Matcher m = NAMESPACE_PATTERN.matcher(content);
if (!m.find()) {
return content;
}
char[] chars = content.toCharArray();
int searchFrom = 0;
m.reset();
while (m.find(searchFrom)) {
int start = m.start();
int bodyStart = m.end();
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
if (bodyEnd <= start) {
searchFrom = Math.max(m.end(), searchFrom + 1);
continue;
}
int endExclusive = Math.min(chars.length, bodyEnd + 1);
for (int i = start; i < endExclusive; i++) {
chars[i] = ' ';
}
searchFrom = endExclusive;
}
return new String(chars);
}
/**
* Parse type parameters from a string like "T extends EntityPlayerMP /* net.minecraft.entity.player.EntityPlayerMP *`/".
* Handles multiple parameters separated by commas.
* Stores strings only - resolution happens in Phase 2 after all types are loaded.
*/
private void parseTypeParameters(String typeParamsStr, JSTypeInfo typeInfo) {
// Split by comma, but be careful with nested generics (shouldn't happen at this level, but be safe)
String[] params = typeParamsStr.split(",");
for (String param : params) {
param = param.trim();
if (param.isEmpty()) continue;
Matcher m = TYPE_PARAM_PATTERN.matcher(param);
if (m.find()) {
String name = m.group(1);
String boundType = m.group(2); // Simple name like "EntityPlayerMP"
String fullBoundType = m.group(3); // Full name like "net.minecraft.entity.player.EntityPlayerMP"
// Store strings only - resolution happens in Phase 2
typeInfo.addTypeParam(new TypeParamInfo(name, boundType, fullBoundType));
}
}
}
/**
* Parse nested types (interfaces and type aliases) within a parent type or namespace.
*/
private void parseNestedTypes(String content, String namespace, String packageName) {
// Parse nested interfaces
Matcher nestedInterfaceMatcher = NESTED_INTERFACE_PATTERN.matcher(content);
while (nestedInterfaceMatcher.find()) {
String interfaceName = nestedInterfaceMatcher.group(1);
String typeParamsStr = nestedInterfaceMatcher.group(2);
String extendsClause = nestedInterfaceMatcher.group(3);
JSTypeInfo typeInfo = new JSTypeInfo(interfaceName, namespace);
JSDocInfo jsDoc = extractJSDocBefore(content, nestedInterfaceMatcher.start());
if (jsDoc != null) {
typeInfo.setJsDocInfo(jsDoc);
}
String javaFqn = findJavaFqn(jsDoc, packageName, typeInfo.getFullName());
if (javaFqn != null && !javaFqn.isEmpty()) {
typeInfo.setJavaFqn(javaFqn);
}
// Parse type parameters
if (typeParamsStr != null && !typeParamsStr.isEmpty()) {
parseTypeParameters(typeParamsStr, typeInfo);
}
if (extendsClause != null) {
String extendsType = extendsClause.trim();
extendsType = extendsType.replaceAll("<[^>]*>", "");
if (extendsType.contains(",")) {
extendsType = extendsType.substring(0, extendsType.indexOf(',')).trim();
}
extendsType = cleanType(extendsType);
typeInfo.setExtends(extendsType);
}
// Find the body of this nested interface
int bodyStart = nestedInterfaceMatcher.end();
int bodyEnd = findMatchingBrace(content, bodyStart - 1);
if (bodyEnd > bodyStart) {
String body = content.substring(bodyStart, bodyEnd);
parseInterfaceBody(body, typeInfo);
}
registry.registerType(typeInfo);
}
// Parse type aliases within this context
parseTypeAliases(content, namespace);
}
private String findJavaFqn(JSDocInfo jsDoc, String packageName, String typeFullName) {
String tagged = extractJavaFqnFromJSDoc(jsDoc);
if (tagged != null && !tagged.isEmpty()) {
return tagged;
}
return buildJavaFqnFromPackage(packageName, typeFullName);
}
private String extractJavaFqnFromJSDoc(JSDocInfo jsDoc) {
if (jsDoc == null) return null;
for (JSDocTag tag : jsDoc.getAllTags()) {
if (tag == null) continue;
if ("javaFqn".equals(tag.getTagName())) {
if (tag.getDescription() != null && !tag.getDescription().trim().isEmpty()) {
return tag.getDescription().trim();
}
if (tag.getTypeName() != null && !tag.getTypeName().trim().isEmpty()) {
return tag.getTypeName().trim();
}
}
}
return null;
}
private String buildJavaFqnFromPackage(String packageName, String typeFullName) {
if (typeFullName == null || typeFullName.isEmpty()) return null;
if (packageName == null || packageName.isEmpty()) return null;
return packageName + "." + typeFullName;
}
private String derivePackageName(String fileName) {
if (fileName == null || fileName.isEmpty()) return null;
String normalized = fileName.replace('\\', '/');
if (normalized.endsWith(".d.ts")) {
normalized = normalized.substring(0, normalized.length() - 5);
}
int lastSlash = normalized.lastIndexOf('/');
if (lastSlash < 0) return null;
String pkgPath = normalized.substring(0, lastSlash);
if (pkgPath.isEmpty()) return null;
return pkgPath.replace('/', '.');
}
/**
* Parse type aliases (export type X = Y).
*/
private void parseTypeAliases(String content, String namespace) {
Matcher m = TYPE_ALIAS_PATTERN.matcher(content);
while (m.find()) {
String aliasName = m.group(1);
String targetType = m.group(2).trim();
// Create a simple type that extends the target
JSTypeInfo typeInfo = new JSTypeInfo(aliasName, namespace);
typeInfo.setExtends(targetType);
registry.registerType(typeInfo);
}
}
/**
* Parse the body of an interface to extract methods and fields.
*/
private void parseInterfaceBody(String body, JSTypeInfo typeInfo) {
// Parse methods
Matcher methodMatcher = METHOD_PATTERN.matcher(body);
while (methodMatcher.find()) {
boolean isStatic = methodMatcher.group(1) != null;
String methodName = methodMatcher.group(2);
String params = methodMatcher.group(3);
String returnType = cleanType(methodMatcher.group(4));
List<JSMethodInfo.JSParameterInfo> parameters = parseParameters(params);
JSMethodInfo method = new JSMethodInfo(methodName, returnType, parameters);
method.setStatic(isStatic);
JSDocInfo jsDoc = extractJSDocBefore(body, methodMatcher.start());
if (jsDoc != null) {
method.setJsDocInfo(jsDoc);
}
typeInfo.addMethod(method);
}
// Parse fields (that aren't method signatures)
Matcher fieldMatcher = FIELD_PATTERN.matcher(body);
while (fieldMatcher.find()) {
boolean isStatic = fieldMatcher.group(1) != null;
String fieldName = fieldMatcher.group(3);
// CNPC+ base event implementation CustomNPCsEvent exposes a public "API" field that
// ends up polluting autocomplete on every event via ICustomNPCsEvent inheritance.
// Keep parsing other fields, but skip this one.
if ("ICustomNPCsEvent".equals(typeInfo.getSimpleName()) && "API".equals(fieldName))
continue;
String fieldType = cleanType(fieldMatcher.group(4));
boolean readonly = fieldMatcher.group(2) != null;
JSFieldInfo field = new JSFieldInfo(fieldName, fieldType, readonly);
field.setStatic(isStatic);
JSDocInfo jsDoc = extractJSDocBefore(body, fieldMatcher.start());
if (jsDoc != null) {
field.setJsDocInfo(jsDoc);
}
typeInfo.addField(field);
}
}
/**
* Parse parameters string into list of parameter info.
*/
private List<JSMethodInfo.JSParameterInfo> parseParameters(String params) {
List<JSMethodInfo.JSParameterInfo> result = new ArrayList<>();
if (params == null || params.trim().isEmpty()) {
return result;
}
// Split by comma, but be careful of nested types like Map<K, V>
List<String> paramParts = splitParameters(params);
for (String part : paramParts) {
part = part.trim();
if (part.isEmpty()) continue;
// Format: name: type or name?: type or ...name: type
int colonIndex = part.indexOf(':');
if (colonIndex > 0) {
String name = part.substring(0, colonIndex).trim().replace("?", "");
String type = cleanType(part.substring(colonIndex + 1).trim());
boolean isVarArg = name.startsWith("...");
if (isVarArg) {
name = name.substring(3).trim();
// Varargs in TS are typed as arrays (e.g. ...codes: number[])
// Strip the array wrapper to get the element type since isVarArg already implies array
if (type.endsWith("[]")) {
type = type.substring(0, type.length() - 2).trim();
} else if (type.startsWith("Array<") && type.endsWith(">")) {
type = type.substring(6, type.length() - 1).trim();
}
}
JSMethodInfo.JSParameterInfo param = new JSMethodInfo.JSParameterInfo(name, type);
param.setVarArg(isVarArg);
result.add(param);
}
}
return result;
}
/**
* Split parameters respecting nested angle brackets.
*/
private List<String> splitParameters(String params) {
List<String> result = new ArrayList<>();
int depth = 0;
int start = 0;
for (int i = 0; i < params.length(); i++) {
char c = params.charAt(i);
if (c == '<' || c == '(' || c == '[') {
depth++;
} else if (c == '>' || c == ')' || c == ']') {
depth--;
} else if (c == ',' && depth == 0) {
result.add(params.substring(start, i));
start = i + 1;
}
}
if (start < params.length()) {
result.add(params.substring(start));
}
return result;
}
/**
* Clean up a type string (remove import() syntax, trim, etc.).
*/
private String cleanType(String type) {
if (type == null) return "any";
return TypeStringNormalizer.stripImportTypeSyntax(type);
}
/**
* Find the matching closing brace.
*/
private int findMatchingBrace(String text, int openBracePos) {
int depth = 1;
for (int i = openBracePos + 1; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '{') depth++;
else if (c == '}') {
depth--;
if (depth == 0) return i;
}
}
return -1;
}
/**
* Read entire content from reader.
*/
private String readFully(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
return sb.toString();
}
private JSDocInfo extractJSDocBefore(String content, int elementStart) {
String jsDocBlock = DTSJSDocParser.extractJSDocBefore(content, elementStart);
if (jsDocBlock != null) {
return DTSJSDocParser.parseJSDocBlock(jsDocBlock);
}
return null;
}
}
@@ -0,0 +1,110 @@
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents parsed JSDoc comment information.
* Captures @type, @param, @return, and other JSDoc tags with their type information.
*/
public class JSDocInfo {
private final String rawComment;
private final int startOffset;
private final int endOffset;
private JSDocTypeTag typeTag;
private final List<JSDocParamTag> paramTags = new ArrayList<>();
private JSDocReturnTag returnTag;
private final List<JSDocSeeTag> seeTags = new ArrayList<>();
private String description;
private final List<JSDocTag> allTags = new ArrayList<>();
public JSDocInfo(String rawComment, int startOffset, int endOffset) {
this.rawComment = rawComment;
this.startOffset = startOffset;
this.endOffset = endOffset;
}
public void setTypeTag(JSDocTypeTag typeTag) {
this.typeTag = typeTag;
allTags.add(typeTag);
}
public void addParamTag(JSDocParamTag paramTag) {
this.paramTags.add(paramTag);
allTags.add(paramTag);
}
public void setReturnTag(JSDocReturnTag returnTag) {
this.returnTag = returnTag;
allTags.add(returnTag);
}
public void addSeeTag(JSDocSeeTag seeTag) {
this.seeTags.add(seeTag);
allTags.add(seeTag);
}
public void setDescription(String description) {
this.description = description;
}
public void addTag(JSDocTag tag) {
allTags.add(tag);
}
public String getRawComment() { return rawComment; }
public int getStartOffset() { return startOffset; }
public int getEndOffset() { return endOffset; }
public JSDocTypeTag getTypeTag() { return typeTag; }
public boolean hasTypeTag() { return typeTag != null; }
public TypeInfo getDeclaredType() {
return typeTag != null ? typeTag.getTypeInfo() : null;
}
public List<JSDocParamTag> getParamTags() {
return Collections.unmodifiableList(paramTags);
}
public boolean hasParamTags() { return !paramTags.isEmpty(); }
public JSDocParamTag getParamTag(String paramName) {
for (JSDocParamTag tag : paramTags) {
String tagName = tag.getParamName();
if (tagName != null && tagName.equals(paramName)) {
return tag;
}
}
return null;
}
public JSDocReturnTag getReturnTag() { return returnTag; }
public boolean hasReturnTag() { return returnTag != null; }
public TypeInfo getReturnType() {
return returnTag != null ? returnTag.getTypeInfo() : null;
}
public List<JSDocSeeTag> getSeeTags() { return Collections.unmodifiableList(seeTags); }
public boolean hasSeeTags() { return !seeTags.isEmpty(); }
public String getDescription() { return description; }
public List<JSDocTag> getAllTags() { return Collections.unmodifiableList(allTags); }
@Override
public String toString() {
StringBuilder sb = new StringBuilder("JSDocInfo{");
if (typeTag != null) sb.append("type=").append(typeTag.getTypeName());
if (!paramTags.isEmpty()) sb.append(", params=").append(paramTags.size());
if (returnTag != null) sb.append(", return=").append(returnTag.getTypeName());
if (!seeTags.isEmpty()) sb.append(", see=").append(seeTags.size());
sb.append("}");
return sb.toString();
}
}
@@ -0,0 +1,43 @@
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
/**
* Represents an @param JSDoc tag.
* Used to declare the type and description of a function parameter.
* Example: @param {string} name - The name of the person
*/
public class JSDocParamTag extends JSDocTag {
private final String paramName;
private final int paramNameStart;
private final int paramNameEnd;
public JSDocParamTag(int atSignOffset, int tagNameStart, int tagNameEnd,
String paramName, int paramNameStart, int paramNameEnd) {
super("param", atSignOffset, tagNameStart, tagNameEnd);
this.paramName = paramName;
this.paramNameStart = paramNameStart;
this.paramNameEnd = paramNameEnd;
}
public static JSDocParamTag create(int atSignOffset, int tagNameStart, int tagNameEnd,
String typeName, TypeInfo typeInfo, int typeStart, int typeEnd,
String paramName, int paramNameStart, int paramNameEnd,
String description) {
JSDocParamTag tag = new JSDocParamTag(atSignOffset, tagNameStart, tagNameEnd,
paramName, paramNameStart, paramNameEnd);
tag.setType(typeName, typeInfo, typeStart, typeEnd);
tag.setDescription(description);
return tag;
}
public String getParamName() { return paramName; }
public int getParamNameStart() { return paramNameStart; }
public int getParamNameEnd() { return paramNameEnd; }
@Override
public String toString() {
return "JSDocParamTag{@param " + (typeName != null ? "{" + typeName + "} " : "") + paramName + "}";
}
}
@@ -0,0 +1,330 @@
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
import noppes.npcs.client.gui.util.script.interpreter.ScriptDocument;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses JSDoc comments to extract type information.
* Handles @type, @param, @return/@returns tags.
*/
public class JSDocParser {
private static final Pattern JSDOC_PATTERN = Pattern.compile(
"/\\*\\*([\\s\\S]*?)\\*/", Pattern.MULTILINE);
private static final Pattern TYPE_TAG_PATTERN = Pattern.compile(
"^@(type)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE);
private static final Pattern PARAM_TAG_PATTERN = Pattern.compile(
"^@param\\s*(?:\\{([^}]+)\\})?\\s*(\\w+)?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE);
private static final Pattern RETURN_TAG_PATTERN = Pattern.compile(
"^@(returns?)\\s*(?:\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE);
private static final Pattern GENERIC_TAG_PATTERN = Pattern.compile(
"^@(\\w+)(?:\\s*\\{([^}]+)\\})?(?:\\s*-?\\s*(.*))?$", Pattern.CASE_INSENSITIVE);
private final ScriptDocument document;
public JSDocParser(ScriptDocument typeResolver) {
this.document = typeResolver;
}
public JSDocInfo parse(String comment, int commentStart) {
if (comment == null || !comment.startsWith("/**")) {
return null;
}
int commentEnd = commentStart + comment.length();
JSDocInfo info = new JSDocInfo(comment, commentStart, commentEnd);
String content = comment.substring(3);
if (content.endsWith("*/")) {
content = content.substring(0, content.length() - 2);
}
int contentOffset = commentStart + 3;
StringBuilder descriptionBuilder = new StringBuilder();
boolean foundFirstTag = false;
JSDocTag lastTag = null;
int position = 0;
while (position <= content.length()) {
int lineEnd = content.indexOf('\n', position);
if (lineEnd == -1) {
lineEnd = content.length();
}
String rawLine = content.substring(position, lineEnd);
if (rawLine.endsWith("\r")) {
rawLine = rawLine.substring(0, rawLine.length() - 1);
}
CleanLine cleanLine = cleanLine(rawLine, contentOffset + position);
String line = cleanLine.text;
if (line.startsWith("@")) {
foundFirstTag = true;
lastTag = parseTagLine(line, cleanLine.offset, info);
} else if (!foundFirstTag) {
if (line.isEmpty()) {
// Preserve blank lines as paragraph gaps (only if description has started)
if (descriptionBuilder.length() > 0) {
descriptionBuilder.append("\n");
}
} else {
if (descriptionBuilder.length() > 0) {
descriptionBuilder.append("\n");
}
descriptionBuilder.append(line);
}
} else if (lastTag != null && !line.isEmpty()) {
String existing = lastTag.getDescription();
if (existing == null || existing.isEmpty()) {
lastTag.setDescription(line);
} else {
lastTag.setDescription(existing + "\n" + line);
}
}
if (lineEnd == content.length()) {
break;
}
position = lineEnd + 1;
}
if (descriptionBuilder.length() > 0) {
info.setDescription(descriptionBuilder.toString().trim());
}
return info;
}
public JSDocInfo extractJSDocBefore(String source, int position) {
if (source == null || position <= 0) {
return null;
}
int searchStart = Math.max(0, position - 1);
while (searchStart > 0 && Character.isWhitespace(source.charAt(searchStart))) {
searchStart--;
}
int endCommentPos = -1;
if (searchStart >= 1 && source.charAt(searchStart) == '/' && source.charAt(searchStart - 1) == '*') {
endCommentPos = searchStart;
} else if (searchStart >= 1 && source.charAt(searchStart - 1) == '/' && searchStart > 1 && source.charAt(
searchStart - 2) == '*') {
endCommentPos = searchStart - 1;
}
if (endCommentPos < 0) {
return null;
}
int startCommentPos = -1;
for (int i = endCommentPos - 1; i >= 2; i--) {
if (source.charAt(i) == '*' && source.charAt(i - 1) == '*' && source.charAt(i - 2) == '/') {
startCommentPos = i - 2;
break;
}
}
if (startCommentPos < 0) {
return null;
}
String comment = source.substring(startCommentPos, endCommentPos + 1);
return parse(comment, startCommentPos);
}
private JSDocTag parseTagLine(String line, int lineOffset, JSDocInfo info) {
Matcher paramMatcher = PARAM_TAG_PATTERN.matcher(line);
if (paramMatcher.matches()) {
String typeName = paramMatcher.group(1);
String paramName = paramMatcher.group(2);
String description = normalizeDescription(paramMatcher.group(3));
int atSignOffset = lineOffset + paramMatcher.start();
int tagNameStart = lineOffset + 1;
int tagNameEnd = tagNameStart + "param".length();
int typeStart = -1;
int typeEnd = -1;
TypeInfo typeInfo = null;
if (typeName != null) {
typeName = typeName.trim();
typeStart = lineOffset + paramMatcher.start(1);
typeEnd = lineOffset + paramMatcher.end(1);
typeInfo = resolveType(typeName);
}
int paramNameStart = -1;
int paramNameEnd = -1;
if (paramName != null) {
paramNameStart = lineOffset + paramMatcher.start(2);
paramNameEnd = lineOffset + paramMatcher.end(2);
}
JSDocParamTag tag = JSDocParamTag.create(atSignOffset, tagNameStart, tagNameEnd,
typeName, typeInfo, typeStart, typeEnd,
paramName, paramNameStart, paramNameEnd,
description);
info.addParamTag(tag);
return tag;
}
Matcher returnMatcher = RETURN_TAG_PATTERN.matcher(line);
if (returnMatcher.matches()) {
String tagName = returnMatcher.group(1);
String typeName = returnMatcher.group(2);
String description = normalizeDescription(returnMatcher.group(3));
int atSignOffset = lineOffset + returnMatcher.start();
int tagNameStart = lineOffset + returnMatcher.start(1);
int tagNameEnd = lineOffset + returnMatcher.end(1);
int typeStart = -1;
int typeEnd = -1;
TypeInfo typeInfo = null;
if (typeName != null) {
typeName = typeName.trim();
typeStart = lineOffset + returnMatcher.start(2);
typeEnd = lineOffset + returnMatcher.end(2);
typeInfo = resolveType(typeName);
}
JSDocReturnTag tag = JSDocReturnTag.create(tagName, atSignOffset, tagNameStart, tagNameEnd,
typeName, typeInfo, typeStart, typeEnd,
description);
info.setReturnTag(tag);
return tag;
}
Matcher typeMatcher = TYPE_TAG_PATTERN.matcher(line);
if (typeMatcher.matches()) {
String typeName = typeMatcher.group(2);
String description = normalizeDescription(typeMatcher.group(3));
int atSignOffset = lineOffset + typeMatcher.start();
int tagNameStart = lineOffset + typeMatcher.start(1);
int tagNameEnd = lineOffset + typeMatcher.end(1);
int typeStart = -1;
int typeEnd = -1;
TypeInfo typeInfo = null;
if (typeName != null) {
typeName = typeName.trim();
typeStart = lineOffset + typeMatcher.start(2);
typeEnd = lineOffset + typeMatcher.end(2);
typeInfo = resolveType(typeName);
}
JSDocTypeTag tag = JSDocTypeTag.create(atSignOffset, tagNameStart, tagNameEnd,
typeName, typeInfo, typeStart, typeEnd, description);
info.setTypeTag(tag);
return tag;
}
Matcher genericMatcher = GENERIC_TAG_PATTERN.matcher(line);
if (genericMatcher.matches()) {
String tagName = genericMatcher.group(1);
String typeName = genericMatcher.group(2);
String description = normalizeDescription(genericMatcher.group(3));
int atSignOffset = lineOffset + genericMatcher.start();
int tagNameStart = lineOffset + genericMatcher.start(1);
int tagNameEnd = lineOffset + genericMatcher.end(1);
JSDocTag tag = new JSDocTag(tagName, atSignOffset, tagNameStart, tagNameEnd);
if (typeName != null) {
typeName = typeName.trim();
int typeStart = lineOffset + genericMatcher.start(2);
int typeEnd = lineOffset + genericMatcher.end(2);
TypeInfo typeInfo = resolveType(typeName);
tag.setType(typeName, typeInfo, typeStart, typeEnd);
}
if (description != null && !description.isEmpty()) {
tag.setDescription(description);
}
info.addTag(tag);
return tag;
}
return null;
}
private String normalizeDescription(String description) {
if (description == null) {
return null;
}
String trimmed = description.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private CleanLine cleanLine(String line, int lineStartOffset) {
int index = 0;
while (index < line.length() && Character.isWhitespace(line.charAt(index))) {
index++;
}
if (index + 2 < line.length() && line.startsWith("/**", index)) {
index += 3;
while (index < line.length() && Character.isWhitespace(line.charAt(index))) {
index++;
}
}
if (index < line.length() && line.charAt(index) == '*') {
index++;
if (index < line.length() && line.charAt(index) == ' ') {
index++;
}
}
String cleaned = line.substring(index);
int end = cleaned.length();
while (end > 0 && Character.isWhitespace(cleaned.charAt(end - 1))) {
end--;
}
cleaned = cleaned.substring(0, end);
return new CleanLine(cleaned, lineStartOffset + index);
}
private TypeInfo resolveType(String typeName) {
return document.resolveType(typeName);
}
private static class CleanLine {
private final String text;
private final int offset;
private CleanLine(String text, int offset) {
this.text = text;
this.offset = offset;
}
}
public static List<int[]> findAllJSDocComments(String source) {
List<int[]> positions = new ArrayList<>();
Matcher m = JSDOC_PATTERN.matcher(source);
while (m.find()) {
positions.add(new int[]{m.start(), m.end()});
}
return positions;
}
}
@@ -0,0 +1,29 @@
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
/**
* Represents an @return or @returns JSDoc tag.
* Used to declare the return type of a function.
* Example: @return {number} The calculated result
*/
public class JSDocReturnTag extends JSDocTag {
public JSDocReturnTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd) {
super(tagName, atSignOffset, tagNameStart, tagNameEnd);
}
public static JSDocReturnTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd,
String typeName, TypeInfo typeInfo, int typeStart, int typeEnd,
String description) {
JSDocReturnTag tag = new JSDocReturnTag(tagName, atSignOffset, tagNameStart, tagNameEnd);
tag.setType(typeName, typeInfo, typeStart, typeEnd);
tag.setDescription(description);
return tag;
}
@Override
public String toString() {
return "JSDocReturnTag{@" + tagName + " " + (typeName != null ? "{" + typeName + "}" : "") + "}";
}
}
@@ -0,0 +1,75 @@
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
public class JSDocSeeTag extends JSDocTag {
private final String reference;
private String url;
private String linkText;
public JSDocSeeTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd, String reference) {
super(tagName, atSignOffset, tagNameStart, tagNameEnd);
this.reference = reference;
this.setDescription(reference);
parseReference(reference);
}
public static JSDocSeeTag create(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd,
String reference) {
return new JSDocSeeTag(tagName, atSignOffset, tagNameStart, tagNameEnd, reference);
}
public static JSDocSeeTag createSimple(String reference) {
return new JSDocSeeTag("see", -1, -1, -1, reference);
}
private void parseReference(String ref) {
if (ref == null) return;
if (ref.contains("<a href=")) {
int hrefStart = ref.indexOf("href=\"");
if (hrefStart != -1) {
hrefStart += 6;
int hrefEnd = ref.indexOf("\"", hrefStart);
if (hrefEnd != -1) {
this.url = ref.substring(hrefStart, hrefEnd);
}
}
int textStart = ref.indexOf(">");
int textEnd = ref.indexOf("</a>");
if (textStart != -1 && textEnd != -1 && textStart < textEnd) {
this.linkText = ref.substring(textStart + 1, textEnd);
}
} else if (ref.contains("{@link")) {
int linkStart = ref.indexOf("{@link");
int linkEnd = ref.indexOf("}", linkStart);
if (linkStart != -1 && linkEnd != -1) {
this.linkText = ref.substring(linkStart + 6, linkEnd).trim();
}
}
}
public String getReference() {
return reference;
}
public String getUrl() {
return url;
}
public boolean hasUrl() {
return url != null && !url.isEmpty();
}
public String getLinkText() {
return linkText;
}
public boolean hasLinkText() {
return linkText != null && !linkText.isEmpty();
}
@Override
public String toString() {
return "JSDocSeeTag{@see " + reference + "}";
}
}
@@ -0,0 +1,55 @@
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
/**
* Base class for JSDoc tags.
* Each tag stores its position information for syntax highlighting.
*/
public class JSDocTag {
protected final String tagName;
protected final int atSignOffset;
protected final int tagNameStart;
protected final int tagNameEnd;
protected String typeName;
protected TypeInfo typeInfo;
protected int typeStart = -1;
protected int typeEnd = -1;
protected String description;
public JSDocTag(String tagName, int atSignOffset, int tagNameStart, int tagNameEnd) {
this.tagName = tagName;
this.atSignOffset = atSignOffset;
this.tagNameStart = tagNameStart;
this.tagNameEnd = tagNameEnd;
}
public void setType(String typeName, TypeInfo typeInfo, int typeStart, int typeEnd) {
this.typeName = typeName;
this.typeInfo = typeInfo;
this.typeStart = typeStart;
this.typeEnd = typeEnd;
}
public void setDescription(String description) {
this.description = description;
}
public String getTagName() { return tagName; }
public int getAtSignOffset() { return atSignOffset; }
public int getTagNameStart() { return tagNameStart; }
public int getTagNameEnd() { return tagNameEnd; }
public String getTypeName() { return typeName; }
public TypeInfo getTypeInfo() { return typeInfo; }
public boolean hasType() { return typeName != null && !typeName.isEmpty(); }
public int getTypeStart() { return typeStart; }
public int getTypeEnd() { return typeEnd; }
public String getDescription() { return description; }
@Override
public String toString() {
return "JSDocTag{@" + tagName + (typeName != null ? " {" + typeName + "}" : "") + "}";
}
}
@@ -0,0 +1,28 @@
package noppes.npcs.client.gui.util.script.interpreter.jsdoc;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
/**
* Represents an @type JSDoc tag.
* Used to declare the type of a variable.
* Example: @type {string}
*/
public class JSDocTypeTag extends JSDocTag {
public JSDocTypeTag(int atSignOffset, int tagNameStart, int tagNameEnd) {
super("type", atSignOffset, tagNameStart, tagNameEnd);
}
public static JSDocTypeTag create(int atSignOffset, int tagNameStart, int tagNameEnd, String typeName,
TypeInfo typeInfo, int typeStart, int typeEnd, String description) {
JSDocTypeTag tag = new JSDocTypeTag(atSignOffset, tagNameStart, tagNameEnd);
tag.setType(typeName, typeInfo, typeStart, typeEnd);
tag.setDescription(description);
return tag;
}
@Override
public String toString() {
return "JSDocTypeTag{@type " + (typeName != null ? "{" + typeName + "}" : "") + "}";
}
}
@@ -0,0 +1,458 @@
package noppes.npcs.client.gui.util.script.interpreter.method;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.util.ArrayList;
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
RETURN_TYPE_MISMATCH, // Return type doesn't match expected type (e.g., assignment LHS)
UNRESOLVED_METHOD, // Method doesn't exist
UNRESOLVED_RECEIVER // Can't resolve the receiver type
}
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 TypeInfo expectedType; // Expected return type (from assignment LHS, etc.)
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<>();
private boolean isConstructor;
// For Java.type() ClassTypeInfo constructor access
// i.e var IntArray = Java.type("int[]"); var array = new IntArray()
public boolean isClassTypeAccess;
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;
}
/**
* Factory method to create a MethodCallInfo for a constructor call.
* Constructors are represented as method calls where the type itself is the receiver.
*/
public static MethodCallInfo constructor(TypeInfo typeInfo, MethodInfo constructor,
int typeNameStart, int typeNameEnd,
int openParenOffset, int closeParenOffset,
List<Argument> arguments) {
return new MethodCallInfo(
typeInfo.getSimpleName(), // Constructor name is the type name
typeNameStart,
typeNameEnd,
openParenOffset,
closeParenOffset,
arguments,
typeInfo, // The type itself is the receiver
constructor, // The constructor MethodInfo
false // Constructors are not static access
).setConstructor(true);
}
// Getters
public String getMethodName() {
return methodName;
}
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 TypeInfo getExpectedType() {
return expectedType;
}
public void setExpectedType(TypeInfo expectedType) {
this.expectedType = expectedType;
}
// Dynamically resolved return type (e.g., for Java.type() which returns ClassTypeInfo)
private TypeInfo resolvedReturnType;
/**
* Get the resolved return type of this method call.
* This may differ from resolvedMethod.getReturnType() for methods with dynamic return types.
* @return The resolved return type, or the method's declared return type if not dynamically resolved
*/
public TypeInfo getResolvedReturnType() {
if (resolvedReturnType != null) {
return resolvedReturnType;
}
return resolvedMethod != null ? resolvedMethod.getReturnType() : null;
}
/**
* Set a dynamically resolved return type.
* Used for methods like Java.type() where the return type depends on the arguments.
*/
public void setResolvedReturnType(TypeInfo returnType) {
this.resolvedReturnType = returnType;
}
public boolean isConstructor() {
return isConstructor;
}
public MethodCallInfo setConstructor(boolean isConstructor) {
this.isConstructor = isConstructor;
return this;
}
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();
}
/**
* Check if this is a static access error (underline method name).
*/
public boolean hasStaticAccessError() {
return errorType == ErrorType.STATIC_ACCESS_ERROR;
}
/**
* Check if this is a return type mismatch error.
*/
public boolean hasReturnTypeMismatch() {
return errorType == ErrorType.RETURN_TYPE_MISMATCH;
}
// Setters for validation results
public void setError(ErrorType type, String message) {
this.errorType = type;
this.errorMessage = message;
}
public void setArgTypeError(int argIndex, String message) {
this.argumentTypeErrors.add(new ArgumentTypeError(arguments.get(argIndex), argIndex, message));
}
public List<ArgumentTypeError> getArgumentTypeErrors() {
return argumentTypeErrors;
}
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 (isConstructor) {
if (resolvedMethod == null) {
// Don't report missing JS constructors as unresolved method error, since they may be created dynamically at runtime
boolean isJSType = receiverType != null && receiverType.isJSType();
if (!isJSType && !isClassTypeAccess) {
if (receiverType != null && receiverType.hasConstructors()) {
setError(ErrorType.WRONG_ARG_COUNT,
"No constructor in '" + receiverType.getSimpleName() + "' matches " + arguments.size() + " argument(s)");
} else {
setError(ErrorType.UNRESOLVED_METHOD,
"Cannot resolve constructor for '" + methodName + "'");
}
}
} else if (receiverType != null) {
List<FieldInfo> cParams = resolvedMethod.getParameters();
boolean ctorVarArg = !cParams.isEmpty() && cParams.get(cParams.size() - 1).isVarArg();
boolean ctorCountOk = ctorVarArg
? arguments.size() >= cParams.size() - 1
: arguments.size() == resolvedMethod.getParameterCount();
if (ctorCountOk) validateArgTypeError();
}
return;
}
// Check static/instance access (skip for constructors)
// NO LONGER CHECKED HERE, BUT DIRECTLY AT MARK CREATION
if (!isConstructor && isStaticAccess && !resolvedMethod.isStatic()) {
setError(ErrorType.STATIC_ACCESS_ERROR,
"Cannot call instance method '" + methodName + "' on a class type");
return;
}
List<FieldInfo> params = resolvedMethod.getParameters();
int expectedCount = params.size();
int actualCount = arguments.size();
boolean isVarArgMethod = expectedCount > 0 && params.get(expectedCount - 1).isVarArg();
// Check arg count
if (isVarArgMethod) {
if (actualCount < expectedCount - 1) {
setError(ErrorType.WRONG_ARG_COUNT,
"Expected at least " + (expectedCount - 1) + " argument(s) but got " + actualCount);
return;
}
} else {
if (actualCount != expectedCount) {
setError(ErrorType.WRONG_ARG_COUNT,
"Expected " + expectedCount + " argument(s) but got " + actualCount);
return;
}
}
// Check each argument type
validateArgTypeError();
// Check return type compatibility with expected type (e.g., assignment LHS)
// ALREADY CHECKED AT AssignmentInfo LEVEL
if (expectedType != null && resolvedMethod != null) {
TypeInfo returnType = resolvedMethod.getReturnType();
if (returnType != null && !TypeChecker.isTypeCompatible(expectedType, returnType)) {
// setError(ErrorType.RETURN_TYPE_MISMATCH,
// "Required type: " + expectedType.getSimpleName() + ", Provided: " + returnType.getSimpleName());
}
}
}
public void validateArgTypeError() {
List<FieldInfo> params = resolvedMethod.getParameters();
int paramCount = params.size();
boolean isVarArgMethod = paramCount > 0 && params.get(paramCount - 1).isVarArg();
for (int i = 0; i < arguments.size(); i++) {
Argument arg = arguments.get(i);
// Surface argument-level errors (e.g., SAM conflict, ambiguous overload, bare method in Java)
if (!arg.isValid() && arg.getErrorMessage() != null) {
setArgTypeError(i, arg.getErrorMessage());
continue; // Skip remaining checks for this argument
}
// Varargs: all args at index >= (paramCount-1) map to the element type of the last param
FieldInfo para;
TypeInfo paramType;
if (isVarArgMethod && i >= paramCount - 1) {
para = params.get(paramCount - 1);
TypeInfo arrType = para.getTypeInfo();
paramType = (arrType != null && arrType.isArray()) ? arrType.getElementType() : arrType;
} else {
para = params.get(i);
paramType = para.getTypeInfo();
}
TypeInfo argType = arg.getResolvedType();
if (argType != null && paramType != null) {
if (!TypeChecker.isTypeCompatible(paramType, argType)) {
setArgTypeError(i, "Expected " + paramType.getDisplayName() +
" but got " + argType.getDisplayName());
}
} else if (paramType == null) {
setArgTypeError(i, "Parameter type of '" + para.getName() + "' is unresolved");
} else if (argType == null) {
setArgTypeError(i, "Cannot resolve type of argument '" + arg.getText() + "'");
}
}
}
@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();
}
}
@@ -0,0 +1,746 @@
package noppes.npcs.client.gui.util.script.interpreter.method;
import noppes.npcs.client.gui.util.script.interpreter.*;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.bridge.DtsJavaBridge;
import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSMethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.JSTypeRegistry;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import noppes.npcs.client.gui.util.script.interpreter.type.GenericContext;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeSubstitutor;
import noppes.npcs.client.gui.util.script.interpreter.type.GenericParameterAdapter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
/**
* Metadata for a method declaration or method call.
* Tracks method name, parameters, return type, and containing class.
*
* <p>This class delegates to helper classes for specific functionality:</p>
* <ul>
* <li>{@link ControlFlowAnalyzer} - Control flow analysis for missing return detection</li>
* <li>{@link CodeParser} - Code parsing utilities (brace matching, comment removal)</li>
* <li>{@link TypeChecker} - Type compatibility checking</li>
* </ul>
*/
public final class MethodInfo {
/**
* Validation error types for method declarations.
*/
public enum ErrorType {
NONE,
MISSING_RETURN, // Non-void method missing return statement,
INTERFACE_METHOD_BODY, // Interface method has a body
MISSING_BODY, // Non-void method missing return statement
RETURN_TYPE_MISMATCH, // Return statement type doesn't match method return type
VOID_METHOD_RETURNS_VALUE, // Void method returns a value
DUPLICATE_METHOD, // Method with same signature already defined in scope
DUPLICATE_PARAMETER, // Two parameters have the same name
PARAMETER_UNDEFINED, // Parameter type cannot be resolved
SAM_TYPE_INCOMPATIBLE // Explicit param type incompatible with SAM context
}
private final String name;
private final TypeInfo returnType;
private final TypeInfo containingType; // The class/interface that owns this method
private final List<FieldInfo> parameters;
private final int fullDeclarationOffset; // Start of full declaration (including modifiers), -1 for external
private final int typeOffset; // Start of return type
private final int nameOffset; // Start of method name
private final int bodyStart; // Start of method body (after {)
private final int bodyEnd; // End of method body (before })
private final boolean resolved;
private final boolean isDeclaration; // true if this is a declaration, false if it's a call
private final int modifiers; // Java Modifier flags (e.g., Modifier.PUBLIC | Modifier.STATIC)
private final String documentation; // Javadoc/comment documentation for this method
private JSDocInfo jsDocInfo; // Parsed JSDoc info for this method (may be null)
private final java.lang.reflect.Method javaMethod; // The Java reflection Method, if this was created from reflection
// Error tracking for method declarations
private ErrorType errorType = ErrorType.NONE;
private String errorMessage;
private List<ParameterError> parameterErrors = new ArrayList<>();
private List<ReturnStatementError> 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<FieldInfo> parameters, int fullDeclarationOffset, int typeOffset, int nameOffset,
int bodyStart, int bodyEnd, boolean resolved, boolean isDeclaration,
int modifiers, String documentation, java.lang.reflect.Method javaMethod) {
this.name = name;
this.returnType = returnType;
this.containingType = containingType;
this.parameters = parameters != null ? new ArrayList<>(parameters) : new ArrayList<>();
this.fullDeclarationOffset = fullDeclarationOffset;
this.typeOffset = typeOffset;
this.nameOffset = nameOffset;
this.bodyStart = bodyStart;
this.bodyEnd = bodyEnd;
this.resolved = resolved;
this.isDeclaration = isDeclaration;
this.modifiers = modifiers;
this.documentation = documentation;
this.javaMethod = javaMethod;
}
public static MethodInfo declaration(String name, TypeInfo containingType, TypeInfo returnType, List<FieldInfo> params,
int fullDeclOffset, int typeOffset, int nameOffset,
int bodyStart, int bodyEnd, int modifiers, String documentation) {
return new MethodInfo(name, returnType, containingType, params, fullDeclOffset, typeOffset, nameOffset, bodyStart, bodyEnd, true, true, modifiers, documentation, null);
}
public static MethodInfo call(String name, TypeInfo containingType, int paramCount) {
boolean resolved = containingType != null && containingType.isResolved() &&
containingType.hasMethod(name);
List<FieldInfo> params = new ArrayList<>();
for (int i = 0; i < paramCount; i++) {
params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER));
}
return new MethodInfo(name, null, containingType, params, -1, -1, -1, -1, -1, resolved, false, 0, null, null);
}
public static MethodInfo unresolvedCall(String name, int paramCount) {
List<FieldInfo> params = new ArrayList<>();
for (int i = 0; i < paramCount; i++) {
params.add(FieldInfo.unresolved("arg" + i, FieldInfo.Scope.PARAMETER));
}
return new MethodInfo(name, null, null, params, -1, -1, -1, -1, -1, false, false, 0, null, null);
}
/**
* Create a MethodInfo for a synthetic/external method.
* Used for built-in types like Nashorn's Java object.
*/
public static MethodInfo external(String name, TypeInfo returnType, TypeInfo containingType,
List<FieldInfo> params, int modifiers, String documentation) {
return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, null);
}
/**
* Create a MethodInfo from reflection data.
* Used when resolving method calls on known types.
* Preserves generic type information from reflection.
*/
public static MethodInfo fromReflection(Method method, TypeInfo containingType) {
String name = method.getName();
// Use getGenericReturnType() to preserve generic information like List<String>
TypeInfo returnType = TypeInfo.fromGenericType(method.getGenericReturnType());
if (returnType == null) {
returnType = TypeInfo.fromClass(method.getReturnType());
}
int modifiers = method.getModifiers();
// Try to find matching JSMethodInfo to bridge parameter names/docs and allow return overrides
JSMethodInfo jsMethod = DtsJavaBridge.findMatchingMethod(method, containingType);
if (jsMethod != null) {
// If .d.ts provides a more specific return type, use it for editor typing.
// (e.g., .d.ts override like IDBCPlayer -> IDBCAddon in npcdbc).
TypeInfo overrideReturnType = DtsJavaBridge.resolveReturnTypeOverride(method, containingType, jsMethod);
if (overrideReturnType != null && overrideReturnType.getJavaClass() != null) {
Class<?> reflectedReturn = method.getReturnType();
Class<?> overrideClass = overrideReturnType.getJavaClass();
if (reflectedReturn.isAssignableFrom(overrideClass)) {
if (reflectedReturn != overrideClass) {
returnType = overrideReturnType;
System.out.println("[DtsJavaBridge] Overrode return type for "
+ method.getDeclaringClass().getName() + "." + name
+ "(" + method.getParameterCount() + ") from "
+ reflectedReturn.getName() + " to " + overrideClass.getName());
}
}
}
}
// If the receiver is parameterized (e.g., List<String>), substitute class type variables (E -> String)
Map<String, TypeInfo> receiverBindings = null;
if (GenericContext.hasGenerics(containingType)) {
receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType);
if (!receiverBindings.isEmpty()) {
returnType = TypeSubstitutor.substitute(returnType, receiverBindings);
}
}
List<FieldInfo> params = new ArrayList<>();
// Use getGenericParameterTypes() to preserve generic information
java.lang.reflect.Type[] genericParamTypes = method.getGenericParameterTypes();
List<JSMethodInfo.JSParameterInfo> jsParams = jsMethod != null ? jsMethod.getParameters() : null;
for (int i = 0; i < genericParamTypes.length; i++) {
TypeInfo paramType = TypeInfo.fromGenericType(genericParamTypes[i]);
if (paramType == null) {
paramType = TypeInfo.fromClass(method.getParameterTypes()[i]);
}
if (receiverBindings != null && !receiverBindings.isEmpty()) {
paramType = TypeSubstitutor.substitute(paramType, receiverBindings);
}
// Adapt Object params to receiver generic type args (e.g., Map.get(Object) -> Map.get(K))
TypeInfo adapted = GenericParameterAdapter.adaptParameterType(method, containingType, i, paramType);
if (adapted != null) {
paramType = adapted;
}
String paramName = "arg" + i;
if (jsParams != null && i < jsParams.size()) {
String jsName = jsParams.get(i).getName();
if (jsName != null && !jsName.isEmpty()) {
paramName = jsName;
}
}
FieldInfo fieldInfo = FieldInfo.reflectionParam(paramName, paramType);
boolean isVarArg = method.isVarArgs() && i == genericParamTypes.length - 1;
if (jsParams != null && i < jsParams.size()) {
isVarArg = isVarArg || jsParams.get(i).isVarArg();
}
fieldInfo.setVarArg(isVarArg);
params.add(fieldInfo);
}
String documentation = null;
JSDocInfo jsDocInfo = null;
if (jsMethod != null) {
jsDocInfo = jsMethod.getJsDocInfo();
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
documentation = jsDocDesc != null ? jsDocDesc : jsMethod.getDocumentation();
}
MethodInfo methodInfo = new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, false, modifiers, documentation, method);
if (jsDocInfo != null) {
methodInfo.setJSDocInfo(jsDocInfo);
}
return methodInfo;
}
/**
* Create a MethodInfo from a Constructor via reflection.
* Used when resolving constructor calls on external types.
* Preserves generic type information for constructor parameters.
*/
public static MethodInfo fromReflectionConstructor(Constructor<?> constructor, TypeInfo containingType) {
String name = containingType.getSimpleName(); // Constructor name is the type name
TypeInfo returnType = containingType; // Constructor "returns" an instance of the type
int modifiers = constructor.getModifiers();
List<FieldInfo> params = new ArrayList<>();
// Use getGenericParameterTypes() to preserve generic information
java.lang.reflect.Type[] genericParamTypes = constructor.getGenericParameterTypes();
for (int i = 0; i < genericParamTypes.length; i++) {
TypeInfo paramType = TypeInfo.fromGenericType(genericParamTypes[i]);
if (paramType == null) {
paramType = TypeInfo.fromClass(constructor.getParameterTypes()[i]);
}
FieldInfo fieldInfo = FieldInfo.reflectionParam("arg" + i, paramType);
fieldInfo.setVarArg(constructor.isVarArgs() && i == genericParamTypes.length - 1);
params.add(fieldInfo);
}
return new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true, true, modifiers, null, null);
}
/**
* Create a MethodInfo from a JSMethodInfo (parsed from .d.ts files).
* Used when resolving method calls on JavaScript types.
*
* @param jsMethod The JavaScript method info from the type registry
* @param containingType The TypeInfo that owns this method
* @return A MethodInfo representing the JavaScript method
*/
public static MethodInfo fromJSMethod(JSMethodInfo jsMethod, TypeInfo containingType) {
String name = jsMethod.getName();
// Use the new getResolvedReturnType method
TypeInfo returnType = jsMethod.getResolvedReturnType(containingType);
// Convert JS parameters to FieldInfo
List<FieldInfo> params = new ArrayList<>();
List<JSMethodInfo.JSParameterInfo> jsParams = jsMethod.getParameters();
for (JSMethodInfo.JSParameterInfo param : jsParams) {
String paramName = param.getName();
// Use the new getResolvedType method
TypeInfo paramType = param.getResolvedType(containingType);
FieldInfo fieldInfo = FieldInfo.reflectionParam(paramName, paramType);
fieldInfo.setVarArg(param.isVarArg());
params.add(fieldInfo);
}
// JS methods are always public (no access modifiers in .d.ts)
int modifiers = Modifier.PUBLIC;
if (jsMethod.isStatic()) {
modifiers |= Modifier.STATIC;
}
// Use the documentation from the method if available
JSDocInfo jsDocInfo = jsMethod.getJsDocInfo();
String jsDocDesc = jsDocInfo != null ? jsDocInfo.getDescription() : null;
String documentation = jsDocDesc != null ? jsDocDesc : jsMethod.getDocumentation();
MethodInfo methodInfo = new MethodInfo(name, returnType, containingType, params, -1, -1, -1, -1, -1, true,
false, modifiers, documentation, null);
methodInfo.setJSDocInfo(jsDocInfo);
return methodInfo;
}
// Getters
public String getName() { return name; }
public TypeInfo getReturnType() { return returnType; }
public TypeInfo getContainingType() { return containingType; }
public List<FieldInfo> getParameters() { return Collections.unmodifiableList(parameters); }
public int getParameterCount() { return parameters.size(); }
public Method getJavaMethod() { return javaMethod; }
/** @deprecated Use getTypeOffset() or getNameOffset() instead */
@Deprecated
public int getDeclarationOffset() { return typeOffset; }
public int getFullDeclarationOffset() { return fullDeclarationOffset; }
public int getTypeOffset() { return typeOffset; }
public int getNameOffset() { return nameOffset; }
public int getBodyStart() { return bodyStart; }
public int getBodyEnd() { return bodyEnd; }
public boolean isResolved() { return resolved; }
public boolean isDeclaration() { return isDeclaration; }
public boolean isCall() { return !isDeclaration; }
public int getModifiers() { return modifiers; }
public boolean isStatic() { return Modifier.isStatic(modifiers); }
public boolean isFinal() { return Modifier.isFinal(modifiers); }
public boolean isAbstract() { return Modifier.isAbstract(modifiers); }
public boolean isSynchronized() { return Modifier.isSynchronized(modifiers); }
public boolean isNative() { return Modifier.isNative(modifiers); }
public boolean isPublic() { return Modifier.isPublic(modifiers); }
public boolean isPrivate() { return Modifier.isPrivate(modifiers); }
public boolean isProtected() { return Modifier.isProtected(modifiers); }
public String getDocumentation() { return documentation; }
public JSDocInfo getJSDocInfo() { return jsDocInfo; }
public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; }
// ==================== 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.
*/
public boolean containsPosition(int position) {
return position >= bodyStart && position < bodyEnd;
}
/**
* Get the end of the method declaration (closing paren position).
* This is used for error highlighting of duplicate methods.
*/
public int getDeclarationEnd() {
// The declaration ends just before the opening brace
// We find the closing paren by searching backwards from bodyStart
return bodyStart > 0 ? bodyStart - 1 : nameOffset + name.length();
}
/**
* Check if this method has a parameter with the given name.
*/
public boolean hasParameter(String paramName) {
for (FieldInfo p : parameters) {
if (p.getName().equals(paramName)) {
return true;
}
}
return false;
}
/**
* Get parameter info by name.
*/
public FieldInfo getParameter(String paramName) {
for (FieldInfo p : parameters) {
if (p.getName().equals(paramName)) {
return p;
}
}
return null;
}
/**
* Get method signature for duplicate detection.
* Signature = name + parameter types (ignoring parameter names).
*/
public MethodSignature cachedSignature;
public MethodSignature getSignature() {
if (cachedSignature == null) {
List<TypeInfo> paramTypes = new ArrayList<>();
for (FieldInfo param : parameters)
paramTypes.add(param.getTypeInfo());
cachedSignature = new MethodSignature(name, paramTypes);
}
return cachedSignature;
}
/**
* Get the appropriate TokenType for highlighting this method.
*/
public TokenType getTokenType() {
if (isDeclaration) {
return TokenType.METHOD_DECL;
}
return resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("MethodInfo{");
sb.append(name).append("(");
for (int i = 0; i < parameters.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(parameters.get(i).getName());
}
sb.append(")");
if (returnType != null) {
sb.append(" -> ").append(returnType.getSimpleName());
}
sb.append(", ").append(isDeclaration ? "decl" : "call");
sb.append(", ").append(resolved ? "resolved" : "unresolved");
sb.append("}");
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MethodInfo that = (MethodInfo) o;
return name.equals(that.name) && parameters.size() == that.parameters.size();
}
@Override
public int hashCode() {
return name.hashCode() * 31 + parameters.size();
}
// ==================== ERROR HANDLING ====================
/**
* Represents an error with a specific parameter.
*/
public static class ParameterError {
private final FieldInfo parameter;
private final int paramIndex;
private final ErrorType errorType;
private final String message;
public ParameterError(FieldInfo parameter, int paramIndex, ErrorType errorType, String message) {
this.parameter = parameter;
this.paramIndex = paramIndex;
this.errorType = errorType;
this.message = message;
}
public FieldInfo getParameter() { return parameter; }
public int getParamIndex() { return paramIndex; }
public ErrorType getErrorType() { return errorType; }
public String getMessage() { return message; }
}
/**
* Represents an error with a return statement (type mismatch).
*/
public static class ReturnStatementError {
private final int startOffset; // Start of "return" keyword (absolute position)
private final int endOffset; // End of return statement (after semicolon)
private final String message;
private final TypeInfo expectedType;
private final TypeInfo actualType;
public ReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) {
this.startOffset = startOffset;
this.endOffset = endOffset;
this.message = message;
this.expectedType = expectedType;
this.actualType = actualType;
}
public int getStartOffset() { return startOffset; }
public int getEndOffset() { return endOffset; }
public String getMessage() { return message; }
public TypeInfo getExpectedType() { return expectedType; }
public TypeInfo getActualType() { return actualType; }
}
/**
* Check if this method declaration has any errors.
*/
public boolean hasError() {
return errorType != ErrorType.NONE || !parameterErrors.isEmpty() || !returnStatementErrors.isEmpty();
}
/**
* Check if this has a missing return error.
*/
public boolean hasMissingReturnError() {
return errorType == ErrorType.MISSING_RETURN;
}
/**
* Check if this has parameter errors.
*/
public boolean hasParameterErrors() {
return !parameterErrors.isEmpty();
}
/**
* Check if this has return statement type errors.
*/
public boolean hasReturnStatementErrors() {
return !returnStatementErrors.isEmpty();
}
public ErrorType getErrorType() { return errorType; }
public String getErrorMessage() { return errorMessage; }
public List<ParameterError> getParameterErrors() { return Collections.unmodifiableList(parameterErrors); }
public List<ReturnStatementError> getReturnStatementErrors() { return Collections.unmodifiableList(returnStatementErrors); }
/**
* Set the main error for this method.
*/
public void setError(ErrorType type, String message) {
this.errorType = type;
this.errorMessage = message;
}
/**
* Add a parameter-specific error.
*/
public void addParameterError(FieldInfo param, int index, ErrorType type, String message) {
parameterErrors.add(new ParameterError(param, index, type, message));
}
public void addSamTypeError(int paramIndex, TypeInfo expectedSamType, TypeInfo actualDeclaredType) {
if (paramIndex < 0 || paramIndex >= parameters.size()) {
return;
}
FieldInfo param = parameters.get(paramIndex);
String message = "Explicit type '" + actualDeclaredType.getSimpleName() +
"' is incompatible with SAM parameter type '" + expectedSamType.getSimpleName() + "'";
parameterErrors.add(new ParameterError(param, paramIndex, ErrorType.SAM_TYPE_INCOMPATIBLE, message));
}
/**
* Add a return statement error.
*/
public void addReturnStatementError(int startOffset, int endOffset, String message, TypeInfo expectedType, TypeInfo actualType) {
returnStatementErrors.add(new ReturnStatementError(startOffset, endOffset, message, expectedType, actualType));
}
/**
* Functional interface for type resolution callback.
*/
@FunctionalInterface
public interface TypeResolver {
TypeInfo resolveExpression(String expression, int position);
}
/**
* Validate this method declaration with type resolution for return statements.
* Checks for parameter errors, missing return statements, and return type mismatches.
*
* @param methodBodyText The text content of the method body (between { and })
* @param typeResolver Optional callback to resolve expression types (for return type checking)
*/
public void validate(String methodBodyText, boolean hasBody, TypeResolver typeResolver) {
if (!isDeclaration) return;
// Don't error if return type unresolved
if(returnType != null && !returnType.isResolved())
return;
// Validate parameters
validateParameters();
boolean interfaceMember = containingType != null && containingType.isInterface();
if (hasBody && (interfaceMember || isAbstract() || isNative())) {
setError(MethodInfo.ErrorType.INTERFACE_METHOD_BODY, "Interface or abstract methods cannot have a body");
return;
}
//No need to check for return types/statements
if(interfaceMember)
return;
//If no body and not interface or abstract/native
if (bodyStart == bodyEnd && !isAbstract() && !isNative()) {
setError(ErrorType.MISSING_BODY, "Method must have a body or be declared abstract/native.");
return;
}
// Validate return statement types FIRST if we have a type resolver
// This ensures type errors are shown even if there's also a missing return
if (typeResolver != null) {
validateReturnTypes(methodBodyText, typeResolver);
}
// Validate return statement for non-void methods
// This may set the main error, but return type errors are already recorded
validateReturnStatement(methodBodyText);
}
/**
* Check for duplicate and unresolved parameters.
*/
private void validateParameters() {
Set<String> seenNames = new HashSet<>();
for (int i = 0; i < parameters.size(); i++) {
FieldInfo param = parameters.get(i);
String paramName = param.getName();
// Check for duplicate parameter names
if (seenNames.contains(paramName)) {
addParameterError(param, i, ErrorType.DUPLICATE_PARAMETER,
"Duplicate parameter name '" + paramName + "'");
} else {
seenNames.add(paramName);
}
// Check for unresolved parameter types
TypeInfo paramType = param.getTypeInfo();
if (paramType == null || !paramType.isResolved()) {
String typeName = paramType != null ? paramType.getDisplayName() : "unknown";
addParameterError(param, i, ErrorType.PARAMETER_UNDEFINED,
"Cannot resolve parameter type '" + typeName + "'");
}
}
}
/**
* Validate that a non-void method has a guaranteed return statement.
* Delegates to {@link ControlFlowAnalyzer} for control flow analysis.
*/
private void validateReturnStatement(String bodyText) {
if (bodyText == null || bodyText.isEmpty()) return;
// void methods don't need return statements
if (TypeChecker.isVoidType(returnType)) return;
// Abstract/native methods don't have bodies
if (isAbstract() || isNative()) return;
// Check if the method has a guaranteed return using the control flow analyzer
if (!ControlFlowAnalyzer.hasGuaranteedReturn(bodyText)) {
setError(ErrorType.MISSING_RETURN, "Missing return statement");
}
}
/**
* Validate that all return statements have compatible types.
* Delegates to {@link CodeParser} for parsing and {@link TypeChecker} for type checks.
*/
private void validateReturnTypes(String bodyText, TypeResolver typeResolver) {
if (bodyText == null || bodyText.isEmpty()) return;
boolean isVoid = TypeChecker.isVoidType(returnType);
String expectedTypeName = isVoid ? "void" : returnType.getDisplayName();
// Remove comments but keep strings (we need accurate positions)
String cleanBody = CodeParser.removeComments(bodyText);
// Find all return statements
int pos = 0;
while (pos < cleanBody.length()) {
// Look for "return" keyword
int returnPos = CodeParser.findReturnKeyword(cleanBody, pos);
if (returnPos < 0) break;
// Find the semicolon ending this return statement
int semiPos = CodeParser.findReturnSemicolon(cleanBody, returnPos + 6);
if (semiPos < 0) {
pos = returnPos + 6;
continue;
}
// Extract the return expression
String returnExpr = cleanBody.substring(returnPos + 6, semiPos).trim();
// Calculate absolute position (bodyStart + 1 is after the opening brace)
int absoluteReturnStart = bodyStart + 1 + returnPos;
int absoluteSemiEnd = bodyStart + 1 + semiPos + 1; // +1 to include semicolon
// Check void method returning a value
if (isVoid && !returnExpr.isEmpty()) {
String message = "Cannot return a value from a method with void result type";
addReturnStatementError(absoluteReturnStart, absoluteSemiEnd, message, returnType, null);
}
// Check non-void method return type compatibility
else if (!isVoid && !returnExpr.isEmpty() && typeResolver != null) {
// Resolve the expression type
TypeInfo actualType = typeResolver.resolveExpression(returnExpr, absoluteReturnStart);
// Check type compatibility
if (!TypeChecker.isTypeCompatible(returnType, actualType)) {
String actualTypeName = actualType != null ? actualType.getSimpleName() : "null";
String message = "Incompatible types.\nRequired: " + expectedTypeName + "\nFound: " + actualTypeName;
addReturnStatementError(absoluteReturnStart, absoluteSemiEnd, message, returnType, actualType);
}
}
pos = semiPos + 1;
}
}
}
@@ -0,0 +1,95 @@
package noppes.npcs.client.gui.util.script.interpreter.method;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a method signature for robust comparison.
* Compares method name and parameter types as structured data, not as strings.
*/
public final class MethodSignature {
private final String methodName;
private final List<TypeInfo> parameterTypes;
public MethodSignature(String methodName, List<TypeInfo> parameterTypes) {
this.methodName = methodName;
this.parameterTypes = new ArrayList<>(parameterTypes);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof MethodSignature))
return false;
MethodSignature other = (MethodSignature) obj;
// Compare method names
if (!this.methodName.equals(other.methodName))
return false;
// Compare parameter count
if (this.parameterTypes.size() != other.parameterTypes.size())
return false;
// Compare each parameter type
for (int i = 0; i < this.parameterTypes.size(); i++) {
TypeInfo thisType = this.parameterTypes.get(i);
TypeInfo otherType = other.parameterTypes.get(i);
// Both null = equal
if (thisType == null && otherType == null)
continue;
// One null, one not = not equal
if (thisType == null || otherType == null)
return false;
// Compare full type names
if (!thisType.getFullName().equals(otherType.getFullName()))
return false;
}
return true;
}
@Override
public int hashCode() {
int result = methodName.hashCode();
for (TypeInfo paramType : parameterTypes) {
result = 31 * result + (paramType != null ? paramType.getFullName().hashCode() : 0);
}
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(methodName);
sb.append("(");
for (int i = 0; i < parameterTypes.size(); i++) {
if (i > 0)
sb.append(", ");
TypeInfo type = parameterTypes.get(i);
sb.append(type != null ? type.getFullName() : "?");
}
sb.append(")");
return sb.toString();
}
public static String asString(Method javaMethod) {
StringBuilder sig = new StringBuilder(javaMethod.getName());
sig.append("(");
Class<?>[] paramTypes = javaMethod.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0)
sig.append(", ");
sig.append(paramTypes[i].getSimpleName());
}
sig.append(")");
return sig.toString();
}
}
@@ -0,0 +1,102 @@
/**
* <h1>Interpreter Package - Java Syntax Highlighting System</h1>
*
* <p>This package provides a clean, modular rewrite of the JavaTextContainer
* syntax highlighting system. It replaces the original implementation with
* well-structured, atomic classes that work together like LEGO pieces.</p>
*
* <h2>Architecture Overview</h2>
*
* <pre>
* ┌─────────────────────────────────────────────────────────────────┐
* │ ScriptTextContainer │
* │ (Compatibility adapter extending JavaTextContainer) │
* │ - USE_NEW_INTERPRETER flag to toggle between systems │
* │ - Produces JavaTextContainer.LineData/Token for GUI compat │
* └───────────────────────┬─────────────────────────────────────────┘
* │
* ▼
* ┌─────────────────────────────────────────────────────────────────┐
* │ ScriptDocument │
* │ (Main document container - replaces JavaTextContainer core) │
* │ │
* │ 7-Phase Tokenization Pipeline: │
* │ 1. findExcludedRanges() - strings/comments │
* │ 2. parseImports() - import statements │
* │ 3. parseStructure() - methods, fields │
* │ 4. buildMarks() - all highlighting marks │
* │ 5. resolveConflicts() - priority-based resolution │
* │ 6. buildTokens() - convert marks to tokens per line │
* │ 7. computeIndentGuides() - visual indent guides │
* └───────────────────────┬─────────────────────────────────────────┘
* │
* ▼
* ┌─────────────────────────────────────────────────────────────────┐
* │ ScriptLine │
* │ (Line container with token management and rendering) │
* │ - Token list with navigation │
* │ - Indent guides │
* │ - drawString() with color codes │
* │ - drawStringHex() for direct hex color rendering │
* └───────────────────────┬─────────────────────────────────────────┘
* │
* ▼
* ┌─────────────────────────────────────────────────────────────────┐
* │ Token │
* │ (Atomic token unit with type-specific metadata) │
* │ - Text, start/end offsets │
* │ - TokenType (with hex color and priority) │
* │ - prev()/next() navigation │
* │ - Type-specific metadata: TypeInfo, FieldInfo, MethodInfo │
* └─────────────────────────────────────────────────────────────────┘
* </pre>
*
* <h2>Supporting Classes</h2>
*
* <ul>
* <li><b>TokenType</b> - Enum with hex colors and priorities for all token types</li>
* <li><b>TypeInfo</b> - Immutable type metadata (class/interface/enum)</li>
* <li><b>TypeResolver</b> - Class resolution with caching (replaces ClassPathFinder)</li>
* <li><b>ImportData</b> - Import statement tracking with resolution status</li>
* <li><b>FieldInfo</b> - Field/variable metadata with scope (global/local/parameter)</li>
* <li><b>MethodInfo</b> - Method declaration/call metadata with parameters</li>
* </ul>
*
* <h2>Key Improvements Over Original</h2>
*
* <ol>
* <li><b>Single-pass tokenization</b> - 7 distinct phases vs multiple loops</li>
* <li><b>Token navigation</b> - prev()/next() methods for easy traversal</li>
* <li><b>Type-specific metadata</b> - Rich information attached to tokens</li>
* <li><b>Hex color support</b> - Direct RGB colors in addition to MC color codes</li>
* <li><b>Clean separation</b> - Each class has one responsibility</li>
* <li><b>Immutable data</b> - TypeInfo, FieldInfo, MethodInfo are immutable</li>
* <li><b>Feature toggle</b> - USE_NEW_INTERPRETER flag for safe rollback</li>
* </ol>
*
* <h2>Usage</h2>
*
* <pre>{@code
* // The new system is automatically used via ScriptTextContainer
* // To disable and use original JavaTextContainer:
* ScriptTextContainer.USE_NEW_INTERPRETER = false;
*
* // To access the underlying ScriptDocument:
* ScriptTextContainer container = (ScriptTextContainer) textArea.getContainer();
* ScriptDocument doc = container.getDocument();
*
* // Token navigation example:
* for (ScriptLine line : doc.getLines()) {
* for (Token token : line.getTokens()) {
* Token next = token.next();
* Token prev = token.prev();
* }
* }
* }</pre>
*
* @since 2.0
* @see ScriptTextContainer
* @see ScriptDocument
* @see Token
*/
package noppes.npcs.client.gui.util.script.interpreter;
@@ -0,0 +1,271 @@
package noppes.npcs.client.gui.util.script.interpreter.token;
import noppes.npcs.client.gui.util.script.interpreter.type.ImportData;
import noppes.npcs.client.gui.util.script.interpreter.ScriptLine;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
/**
* Represents a single token in the source code with its type and metadata.
* Tokens are the atomic units of the syntax highlighting system.
*
* Each token knows:
* - Its text content
* - Its position in the global source
* - Its type (for coloring)
* - Type-specific metadata (resolved class info, declaration info, etc.)
*/
public class Token {
private final String text;
private final int globalStart; // start offset in the full document
private final int globalEnd; // end offset in the full document
private TokenType type;
// Optional metadata based on token type
private TypeInfo typeInfo; // For class references, type declarations
private FieldInfo fieldInfo; // For field references
private MethodInfo methodInfo; // For method calls/declarations
private MethodCallInfo methodCallInfo; // For method calls with argument info
private ImportData importData; // For import statements
private FieldAccessInfo fieldAccessInfo; //For fields like Minecraft.getMinecraft.thePlayer
private TokenErrorMessage errorMessage;
// Navigation - set by ScriptLine
private Token prev;
private Token next;
private ScriptLine parentLine;
public Token(String text, int globalStart, int globalEnd, TokenType type) {
this.text = text;
this.globalStart = globalStart;
this.globalEnd = globalEnd;
this.type = type;
}
// ==================== FACTORY METHODS ====================
public static Token defaultToken(String text, int start, int end) {
return new Token(text, start, end, TokenType.DEFAULT);
}
public static Token keyword(String text, int start, int end) {
return new Token(text, start, end, TokenType.KEYWORD);
}
public static Token modifier(String text, int start, int end) {
return new Token(text, start, end, TokenType.MODIFIER);
}
public static Token comment(String text, int start, int end) {
return new Token(text, start, end, TokenType.COMMENT);
}
public static Token string(String text, int start, int end) {
return new Token(text, start, end, TokenType.STRING);
}
public static Token literal(String text, int start, int end) {
return new Token(text, start, end, TokenType.LITERAL);
}
public static Token typeReference(String text, int start, int end, TypeInfo info) {
Token t = new Token(text, start, end, info != null ? info.getTokenType() : TokenType.TYPE_DECL);
t.typeInfo = info;
return t;
}
public static Token methodDecl(String text, int start, int end, MethodInfo info) {
Token t = new Token(text, start, end, TokenType.METHOD_DECL);
t.methodInfo = info;
return t;
}
public static Token methodCall(String text, int start, int end, MethodInfo info, boolean resolved) {
Token t = new Token(text, start, end, resolved ? TokenType.METHOD_CALL : TokenType.DEFAULT);
t.methodInfo = info;
return t;
}
public static Token globalField(String text, int start, int end, FieldInfo info) {
Token t = new Token(text, start, end, TokenType.GLOBAL_FIELD);
t.fieldInfo = info;
return t;
}
public static Token localField(String text, int start, int end, FieldInfo info) {
Token t = new Token(text, start, end, TokenType.LOCAL_FIELD);
t.fieldInfo = info;
return t;
}
public static Token parameter(String text, int start, int end, FieldInfo info) {
Token t = new Token(text, start, end, TokenType.PARAMETER);
t.fieldInfo = info;
return t;
}
public static Token undefined(String text, int start, int end) {
return new Token(text, start, end, TokenType.UNDEFINED_VAR);
}
// ==================== GETTERS ====================
public String getText() { return text; }
public int getGlobalStart() { return globalStart; }
public int getGlobalEnd() { return globalEnd; }
public int getLength() { return globalEnd - globalStart; }
public TokenType getType() { return type; }
public TypeInfo getTypeInfo() { return typeInfo; }
public FieldInfo getFieldInfo() { return fieldInfo; }
public MethodInfo getMethodInfo() { return methodInfo; }
public MethodCallInfo getMethodCallInfo() { return methodCallInfo; }
public FieldAccessInfo getFieldAccessInfo() { return fieldAccessInfo; }
public ImportData getImportData() { return importData; }
public TokenErrorMessage getErrorMessage() { return errorMessage; }
public ScriptLine getParentLine() { return parentLine; }
// ==================== SETTERS ====================
public void setType(TokenType type) { this.type = type; }
public void setTypeInfo(TypeInfo info) { this.typeInfo = info; }
public void setFieldInfo(FieldInfo info) { this.fieldInfo = info; }
public void setFieldAccessInfo(FieldAccessInfo info) { this.fieldAccessInfo = info; }
public void setMethodInfo(MethodInfo info) { this.methodInfo = info; }
public void setMethodCallInfo(MethodCallInfo info) { this.methodCallInfo = info; }
public void setImportData(ImportData data) { this.importData = data; }
public void setErrorMessage(TokenErrorMessage message) { this.errorMessage = message; }
public void setParentLine(ScriptLine line) { this.parentLine = line; }
public void setPrev(Token prev) { this.prev = prev; }
public void setNext(Token next) { this.next = next; }
// ==================== NAVIGATION ====================
/**
* Get the previous token (may be on a previous line).
*/
public Token prev() {
if (prev != null) return prev;
if (parentLine == null) return null;
ScriptLine prevLine = parentLine.prev();
while (prevLine != null) {
Token last = prevLine.getLastToken();
if (last != null) return last;
prevLine = prevLine.prev();
}
return null;
}
/**
* Get the next token (may be on a following line).
*/
public Token next() {
if (next != null) return next;
if (parentLine == null) return null;
ScriptLine nextLine = parentLine.next();
while (nextLine != null) {
Token first = nextLine.getFirstToken();
if (first != null) return first;
nextLine = nextLine.next();
}
return null;
}
/**
* Get the previous token on the same line only.
*/
public Token prevOnLine() {
return prev;
}
/**
* Get the next token on the same line only.
*/
public Token nextOnLine() {
return next;
}
// ==================== TYPE CHECKS ====================
public boolean isKeyword() {
return type == TokenType.KEYWORD || type == TokenType.MODIFIER ||
type == TokenType.CLASS_KEYWORD || type == TokenType.IMPORT_KEYWORD;
}
public boolean isTypeReference() {
return type == TokenType.TYPE_DECL || type == TokenType.IMPORTED_CLASS ||
type == TokenType.CLASS_DECL || type == TokenType.INTERFACE_DECL ||
type == TokenType.ENUM_DECL;
}
public boolean isEnumConstant() {
return type == TokenType.ENUM_CONSTANT;
}
public boolean isResolved() {
if (typeInfo != null) return typeInfo.isResolved();
if (fieldInfo != null) return fieldInfo.isResolved();
if (methodInfo != null) return methodInfo.isResolved();
return type != TokenType.UNDEFINED_VAR;
}
public boolean isIdentifier() {
if (text.isEmpty()) return false;
char first = text.charAt(0);
return Character.isJavaIdentifierStart(first);
}
public boolean startsWithUpperCase() {
return !text.isEmpty() && Character.isUpperCase(text.charAt(0));
}
public boolean isMethodCall() {
return type == TokenType.METHOD_CALL ||
(type == TokenType.DEFAULT && methodInfo != null);
}
public boolean isField() {
return type == TokenType.GLOBAL_FIELD || type == TokenType.LOCAL_FIELD ||
type == TokenType.PARAMETER || type == TokenType.STATIC_FINAL_FIELD ||
type == TokenType.ENUM_CONSTANT;
}
// ==================== RENDERING ====================
/**
* Get the hex color for this token.
*/
public int getHexColor() {
return type.getHexColor();
}
/**
* Get the Minecraft color code character.
*/
public char getColorCode() {
return type.toColorCode();
}
/**
* Get the Minecraft style prefix for this token (bold/italic codes).
* Returns empty string if no style, otherwise §l for bold, §o for italic, or both.
*/
public String getStylePrefix() {
StringBuilder sb = new StringBuilder();
if (type.isBold()) sb.append('\u00A7').append('l');
if (type.isItalic()) sb.append('\u00A7').append('o');
return sb.toString();
}
@Override
public String toString() {
return "Token{'" + text + "', " + type + ", [" + globalStart + "-" + globalEnd + "]}";
}
}
@@ -0,0 +1,23 @@
package noppes.npcs.client.gui.util.script.interpreter.token;
public class TokenErrorMessage {
private final String message;
public boolean clearOtherErrors;
public TokenErrorMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public static TokenErrorMessage from(String message) {
return new TokenErrorMessage(message);
}
public TokenErrorMessage clearOtherErrors() {
this.clearOtherErrors = true;
return this;
}
}
@@ -0,0 +1,150 @@
package noppes.npcs.client.gui.util.script.interpreter.token;
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
/**
* Defines all token types for syntax highlighting with hex colors and priorities.
* Priority determines which token type wins when marks overlap.
* Higher priority = wins conflicts.
*/
public enum TokenType {
// Comments and strings have highest priority - they override everything inside them
COMMENT(0x777777, 140),
STRING(0xCC8855, 130),
// JSDoc elements - lower priority but will fill gaps left by fragmented comment marking
JSDOC_TAG(0xCC9933, 125), // @param, @type, @return etc. (gold/orange)
JSDOC_TYPE(0x00AAAA, 124), // {TypeName} in JSDoc (aqua like types)
UNUSED_IMPORT(0x666666, 119), // unused import statements (gray)
// Keywords and modifiers
CLASS_KEYWORD(0xFF5555, 115), // 'class', 'interface', 'enum' keywords
IMPORT_KEYWORD(0xFFAA00, 110), // 'import' keyword
KEYWORD(0xFF5555, 100), // control flow: if, else, for, while, etc.
MODIFIER(0xFFAA00, 90), // public, private, static, final, etc.
// Type declarations and references
INTERFACE_DECL(0x55FFFF, 85), // interface names (aqua)
ENUM_DECL(0xFF55FF, 85), // enum names (magenta)
ENUM_CONSTANT(0x55FFFF, 84, true, false), // enum constant values (blue, bold+italic) - like IntelliJ
CLASS_DECL(0x00AAAA, 85), // class names in declarations
IMPORTED_CLASS(0x00AAAA, 75), // imported class usages
TYPE_DECL(0x00AAAA, 70), // package paths, type references
// Methods
METHOD_DECL(0x00AA00, 60), // method declarations (green)
METHOD_CALL(0x55FF55, 50), // method calls (bright green)
// Variables and fields
UNDEFINED_VAR(0xAA0000, 20), // unresolved variables (dark red) - high priority
PARAMETER(0x5555FF, 36), // method parameters (blue)
GLOBAL_FIELD(0x55FFFF, 35), // class-level fields (aqua)
LOCAL_FIELD(0xFFFF55, 25), // local variables (yellow)
STATIC_FINAL_FIELD(0xFF55FF, 36, false, true), // static final fields (magenta, italic)
// Literals
LITERAL(0x777777, 40), // numeric and boolean literals
// Default
VARIABLE(0xFFFFFF, 30), // generic variables
DEFAULT(0xFFFFFF, 0); // default text color (white)
private final int hexColor;
private final int priority;
private final boolean bold;
private final boolean italic;
TokenType(int hexColor, int priority) {
this(hexColor, priority, false, false);
}
TokenType(int hexColor, int priority, boolean bold, boolean italic) {
this.hexColor = hexColor;
this.priority = priority;
this.bold = bold;
this.italic = italic;
}
public int getHexColor() {
return hexColor;
}
public int getPriority() {
return priority;
}
public boolean isBold() {
return bold;
}
public boolean isItalic() {
return italic;
}
public static TokenType getByType(TypeInfo typeInfo) {
if (typeInfo == null || !typeInfo.isResolved())
return TokenType.UNDEFINED_VAR;
if ("any".equals(typeInfo.getFullName()))
return TokenType.KEYWORD;
// Use the TypeInfo's own token type, which handles ScriptTypeInfo correctly
return typeInfo.getTokenType();
}
public static int getColor(TypeInfo typeInfo) {
return getByType(typeInfo).getHexColor();
}
/**
* Convert this token type to a Minecraft color code character.
* Used for backward compatibility with the existing rendering system.
*/
public char toColorCode() {
switch (this) {
case COMMENT:
case LITERAL:
case UNUSED_IMPORT:
return '7'; // gray
case STRING:
return '5'; // purple
case JSDOC_TAG:
return '6'; // gold
case JSDOC_TYPE:
return '3'; // dark aqua (like types)
case CLASS_KEYWORD:
case KEYWORD:
return 'c'; // red
case IMPORT_KEYWORD:
case MODIFIER:
return '6'; // gold
case ENUM_DECL:
case STATIC_FINAL_FIELD:
return 'd'; // magenta
case ENUM_CONSTANT:
return '9'; // blue (same as PARAMETER)
case INTERFACE_DECL:
case GLOBAL_FIELD:
return 'b'; // aqua
case CLASS_DECL:
case IMPORTED_CLASS:
case TYPE_DECL:
return '3'; // dark aqua
case METHOD_DECL:
return '2'; // dark green
case METHOD_CALL:
return 'a'; // green
case LOCAL_FIELD:
return 'e'; // yellow
case PARAMETER:
return '9'; // blue
case UNDEFINED_VAR:
return '4'; // dark red
case VARIABLE:
case DEFAULT:
default:
return 'f'; // white
}
}
}
@@ -0,0 +1,20 @@
/**
* Example Usage of ClassIndex:
*
* 1. Add a single class:
* ClassIndex.getInstance().addClass(EntityPlayer.class);
*
* 2. Add an entire package recursively:
* ClassIndex.getInstance().addPackage("net.minecraft.entity.player");
* This will scan and add ALL classes in that package and subpackages.
*
* 3. Find classes by prefix:
* List<String> matches = ClassIndex.getInstance().findByPrefix("Entity", 50);
* // Returns fully-qualified names like "net.minecraft.entity.Entity", etc.
*
* Benefits:
* - No more manual string pairs! Just pass Class<?> objects.
* - Simple name and package are extracted automatically via reflection.
* - Can scan entire package trees recursively with one call.
* - Cleaner, more maintainable, and easier to extend.
*/
@@ -0,0 +1,326 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Index of Java classes for autocomplete suggestions.
* Uses reflection to automatically extract class names and packages.
* Supports multiple classes with the same simple name (e.g., java.util.Date and java.sql.Date).
*/
public class ClassIndex {
private static ClassIndex instance;
// Map simple names to multiple classes (handles name collisions)
private final Map<String, List<Class<?>>> simpleNameToClasses = new LinkedHashMap<>();
private final Set<Class<?>> registeredClasses = new HashSet<>();
private ClassIndex() {
// Initialize with common classes
initializeCommonClasses();
}
public static ClassIndex getInstance() {
if (instance == null) {
instance = new ClassIndex();
}
return instance;
}
public static void init(){
getInstance();
}
/**
* Register a single class by its Class<?> object.
* The simple name and full name are extracted via reflection.
* Supports multiple classes with the same simple name.
*/
public void addClass(Class<?> clazz) {
if (clazz == null) return;
// Check if already registered using the Class<?> object itself
if (registeredClasses.contains(clazz)) {
return; // Already registered
}
registeredClasses.add(clazz);
String simpleName = clazz.getSimpleName();
// Only add if simple name is not empty (avoid inner classes with $ in name)
if (!simpleName.isEmpty() && !simpleName.contains("$")) {
// Add to list of classes with this simple name
simpleNameToClasses.computeIfAbsent(simpleName, k -> new ArrayList<>()).add(clazz);
}
}
/**
* Register all classes in a package and all subpackages recursively.
* Uses ClassLoader to scan the classpath.
*/
public void addPackage(String packageName) {
addPackage(packageName, new String[0]);
}
/**
* Register all classes in a package and all subpackages recursively,
* excluding specified packages.
* Uses ClassLoader to scan the classpath.
*
* @param packageName The base package to scan
* @param excludedPackages Array of package names to exclude (including their subpackages)
*/
public void addPackage(String packageName, String... excludedPackages) {
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String path = packageName.replace('.', '/');
Enumeration<URL> resources = classLoader.getResources(path);
Set<String> classNames = new HashSet<>();
Set<String> excludedPrefixes = new HashSet<>();
// Prepare excluded package prefixes for matching
for (String excluded : excludedPackages) {
if (excluded != null && !excluded.isEmpty()) {
excludedPrefixes.add(excluded + ".");
}
}
while (resources.hasMoreElements()) {
URL resource = resources.nextElement();
if (resource.getProtocol().equals("file")) {
// Scan directory
File directory = new File(resource.getFile());
scanDirectory(directory, packageName, classNames, excludedPrefixes);
} else if (resource.getProtocol().equals("jar")) {
// Scan JAR file
String jarPath = resource.getPath();
if (jarPath.startsWith("file:")) {
jarPath = jarPath.substring(5);
}
int separatorIndex = jarPath.indexOf("!");
if (separatorIndex != -1) {
jarPath = jarPath.substring(0, separatorIndex);
}
scanJar(jarPath, packageName, classNames, excludedPrefixes);
}
}
// Load all discovered classes
for (String className : classNames) {
try {
Class<?> clazz = Class.forName(className, false, classLoader);
addClass(clazz);
} catch (ClassNotFoundException | NoClassDefFoundError | ExceptionInInitializerError e) {
// Skip classes that can't be loaded
}
}
} catch (Exception e) {
// Silently fail - package might not exist
}
}
private void scanDirectory(File directory, String packageName, Set<String> classNames) {
scanDirectory(directory, packageName, classNames, new HashSet<>());
}
private void scanDirectory(File directory, String packageName, Set<String> classNames, Set<String> excludedPrefixes) {
if (!directory.exists() || !directory.isDirectory()) {
return;
}
// Check if this package is excluded
for (String excludedPrefix : excludedPrefixes) {
if ((packageName + ".").startsWith(excludedPrefix) || packageName.equals(excludedPrefix.substring(0, excludedPrefix.length() - 1))) {
return; // Skip excluded package
}
}
File[] files = directory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
String fileName = file.getName();
if (file.isDirectory()) {
// Recursively scan subdirectory
scanDirectory(file, packageName + "." + fileName, classNames, excludedPrefixes);
} else if (fileName.endsWith(".class")) {
// Add class name
String className = packageName + "." + fileName.substring(0, fileName.length() - 6);
classNames.add(className);
}
}
}
private void scanJar(String jarPath, String packageName, Set<String> classNames) {
scanJar(jarPath, packageName, classNames, new HashSet<>());
}
private void scanJar(String jarPath, String packageName, Set<String> classNames, Set<String> excludedPrefixes) {
try {
JarFile jarFile = new JarFile(jarPath);
Enumeration<JarEntry> entries = jarFile.entries();
String packagePath = packageName.replace('.', '/');
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.startsWith(packagePath) && entryName.endsWith(".class")) {
// Convert path to class name
String className = entryName
.substring(0, entryName.length() - 6)
.replace('/', '.');
// Check if this class is in an excluded package
boolean excluded = false;
for (String excludedPrefix : excludedPrefixes) {
if ((className + ".").startsWith(excludedPrefix) ||
className.startsWith(excludedPrefix.substring(0, excludedPrefix.length() - 1) + ".")) {
excluded = true;
break;
}
}
if (!excluded) {
classNames.add(className);
}
}
}
jarFile.close();
} catch (Exception e) {
// Silently fail
}
}
/**
* Find all classes whose simple name starts with the given prefix (case-insensitive).
* Returns fully-qualified class names for all matches, including multiple classes with the same simple name.
*/
public List<String> findByPrefix(String prefix, int maxResults) {
List<String> results = new ArrayList<>();
String lowerPrefix = prefix.toLowerCase();
for (Map.Entry<String, List<Class<?>>> entry : simpleNameToClasses.entrySet()) {
if (entry.getKey().toLowerCase().startsWith(lowerPrefix)) {
// Add all classes with this simple name
for (Class<?> clazz : entry.getValue()) {
results.add(clazz.getName());
if (maxResults != -1 && results.size() >= maxResults) {
return results;
}
}
}
}
return results;
}
/**
* Initialize the index with commonly used Java, Minecraft, and NPC classes.
*/
private void initializeCommonClasses() {
// Java standard library
addClass(String.class);
addClass(Integer.class);
addClass(Double.class);
addClass(Float.class);
addClass(Long.class);
addClass(Boolean.class);
addClass(Character.class);
addClass(Byte.class);
addClass(Short.class);
addClass(Object.class);
addClass(Math.class);
addClass(System.class);
addClass(Thread.class);
addClass(Runnable.class);
addClass(Exception.class);
addClass(RuntimeException.class);
addClass(Error.class);
addClass(Throwable.class);
// Java collections
addClass(List.class);
addClass(ArrayList.class);
addClass(LinkedList.class);
addClass(Set.class);
addClass(HashSet.class);
addClass(LinkedHashSet.class);
addClass(TreeSet.class);
addClass(Map.class);
addClass(HashMap.class);
addClass(LinkedHashMap.class);
addClass(TreeMap.class);
addClass(Collection.class);
addClass(Iterator.class);
addClass(Comparator.class);
addClass(Collections.class);
addClass(Arrays.class);
addClass(Queue.class);
addClass(Deque.class);
addClass(Stack.class);
addClass(Vector.class);
addClass(Hashtable.class);
// Java I/O
addClass(java.io.File.class);
addClass(java.io.InputStream.class);
addClass(java.io.OutputStream.class);
addClass(java.io.Reader.class);
addClass(java.io.Writer.class);
addClass(java.io.BufferedReader.class);
addClass(java.io.BufferedWriter.class);
addClass(java.io.FileReader.class);
addClass(java.io.FileWriter.class);
addClass(java.io.IOException.class);
// Java utilities
addClass(java.util.Random.class);
addClass(java.util.Date.class);
addClass(java.util.Calendar.class);
addClass(java.util.UUID.class);
addClass(java.util.regex.Pattern.class);
addClass(java.util.regex.Matcher.class);
addPackage("java.util.function");
// Now add common Minecraft/Forge/NPC packages
addPackage("net.minecraft.entity");
addPackage("net.minecraft.item");
addPackage("net.minecraft.block");
addPackage("net.minecraft.world");
addPackage("net.minecraft.util");
addPackage("net.minecraft.nbt");
addPackage("net.minecraft.potion");
addPackage("net.minecraft.enchantment");
addPackage("net.minecraft.inventory");
addPackage("net.minecraft.tileentity");
addPackage("net.minecraft.command");
addPackage("net.minecraft.client");
// Forge
addPackage("net.minecraftforge.common");
addPackage("net.minecraftforge.event");
addPackage("net.minecraftforge.fml.common");
// CustomNPCs
addPackage("noppes.npcs");
// DBC (if available)
try {
addPackage("JinRyuu.JRMCore");
addPackage("JinRyuu.DragonBC");
addPackage("kamkeel.npcdbc");
} catch (Exception e) {
// DBC might not be available
}
}
}
@@ -0,0 +1,158 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents a reference to a Java Class itself (not an instance).
* Used for patterns like: var File = Java.type("java.io.File");
*
* When you have a ClassTypeInfo:
* - Accessing members should show ONLY static members (File.listRoots())
* - Using 'new' should return an instance of the wrapped class (new File("path"))
* - Calling .constructor(...) should return an instance of the wrapped class
*
* This is distinct from a regular TypeInfo which represents an instance of that type.
*/
public class ClassTypeInfo extends TypeInfo {
private final TypeInfo instanceType; // The type you get when you do 'new' on this class
private List<MethodInfo> constructorMethod;
private List<FieldInfo> classField;
/**
* Create a ClassTypeInfo wrapping a Java class.
*
* @param javaClass The Java class this represents (e.g., java.io.File.class)
*/
public ClassTypeInfo(Class<?> javaClass) {
super(
javaClass.getSimpleName(),
javaClass.getName(),
javaClass.getPackage() != null ? javaClass.getPackage().getName() : "",
javaClass.isInterface() ? Kind.INTERFACE : (javaClass.isEnum() ? Kind.ENUM : Kind.CLASS),
javaClass,
true,
null,
true // subclass marker
);
this.instanceType = TypeInfo.fromClass(javaClass);
}
/**
* Create a ClassTypeInfo from an existing TypeInfo.
*/
public ClassTypeInfo(TypeInfo instanceType) {
super(
instanceType.getSimpleName(),
instanceType.getFullName(),
instanceType.getPackageName(),
instanceType.getKind(),
instanceType.getJavaClass(),
instanceType.isResolved(),
instanceType.getEnclosingType(),
true // subclass marker
);
this.instanceType = instanceType;
}
/**
* Returns true - this TypeInfo represents a class reference, not an instance.
*/
@Override
public boolean isClassReference() {
return true;
}
/**
* Get the TypeInfo for instances of this class.
* This is what you get when you call 'new' on this class.
*/
public TypeInfo getInstanceType() {
return instanceType;
}
@Override
public List<MethodInfo> getSyntheticMethods() {
List<MethodInfo> base = super.getSyntheticMethods();
// if (constructorMethod == null) {
constructorMethod = buildConstructorMethod();
//}
if (constructorMethod.isEmpty()) {
return base;
}
List<MethodInfo> merged = new ArrayList<>(base.size() + constructorMethod.size());
merged.addAll(base);
merged.addAll(constructorMethod);
return merged;
}
@Override
public List<FieldInfo> getSyntheticFields() {
List<FieldInfo> base = super.getSyntheticFields();
if (classField == null) {
classField = buildClassField();
}
if (classField.isEmpty()) {
return base;
}
List<FieldInfo> merged = new ArrayList<>(base.size() + classField.size());
merged.addAll(base);
merged.addAll(classField);
return merged;
}
private List<MethodInfo> buildConstructorMethod() {
if (instanceType == null) {
return Collections.emptyList();
}
FieldInfo argsParam = FieldInfo.parameter("args", TypeInfo.OBJECT, -1, null);
argsParam.setVarArg(true);
List<FieldInfo> params = new ArrayList<>();
params.add(argsParam);
MethodInfo ctor = MethodInfo.external(
"constructor",
instanceType,
this,
params,
Modifier.PUBLIC | Modifier.STATIC,
"Creates a new instance of " + instanceType.getSimpleName() + ".\n\n" +
"Equivalent to using the 'new' operator on this class reference."
);
return Collections.singletonList(ctor);
}
private List<FieldInfo> buildClassField() {
if (getJavaClass() == null) {
return Collections.emptyList();
}
try {
TypeInfo classType = TypeInfo.fromClass(Class.class).parameterize(instanceType);
FieldInfo classField = FieldInfo.external("class", classType,
"The Class object for " + getSimpleName(), Modifier.PUBLIC | Modifier.STATIC);
return Collections.singletonList(classField);
} catch (Exception e) {
return Collections.emptyList();
}
}
@Override
public String toString() {
return "Class<" + getSimpleName() + ">";
}
}
@@ -0,0 +1,276 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A context for resolving and substituting declared type parameters (T, E, K, V...)
* based on a receiver type.
*
* Policy:
* - Prefer applied type arguments when present (e.g. List<String> binds E -> String).
* - Fall back to declared bounds when no applied argument exists (e.g. T extends Entity -> Entity).
*/
public final class GenericContext {
/** Singleton context for types with no generic parameters. Used to avoid object allocation. */
private static final GenericContext EMPTY = new GenericContext(new HashMap<>(), new HashMap<>());
/**
* Map of type variable names to their applied type arguments.
* For example, in List&lt;String&gt;, this contains E -&gt; TypeInfo.STRING.
* Applied bindings take precedence over bound fallbacks.
*/
private final Map<String, TypeInfo> appliedBindings;
/**
* Map of type variable names to their declared upper bounds.
* For example, in &lt;T extends Entity&gt;, this contains T -&gt; TypeInfo(Entity).
* Used as fallback when no applied argument exists.
*/
private final Map<String, TypeInfo> boundFallbacks;
private GenericContext(Map<String, TypeInfo> appliedBindings, Map<String, TypeInfo> boundFallbacks) {
this.appliedBindings = appliedBindings != null ? appliedBindings : new HashMap<>();
this.boundFallbacks = boundFallbacks != null ? boundFallbacks : new HashMap<>();
}
/**
* Create a GenericContext for a receiver type.
*
* Fast path: returns singleton EMPTY for non-generic types (no allocation overhead).
* Slow path: builds maps from type parameters and applied arguments for generic types.
*
* @param receiverType the type to extract generic bindings from (e.g., List&lt;String&gt;, DAO&lt;T extends Entity&gt;)
* @return a context with applied bindings and bound fallbacks, or EMPTY singleton if receiverType is not generic
*/
public static GenericContext forReceiver(TypeInfo receiverType) {
// Special edge case: Array.d.ts JS type, bind T to element type so Array<T> methods resolve correctly
if (receiverType != null && receiverType.isArray() && receiverType.getElementType() != null) {
Map<String, TypeInfo> applied = new HashMap<>();
applied.put("T", receiverType.getElementType());
return new GenericContext(applied, new HashMap<>());
}
// Fast path: return singleton for non-generic types
if (receiverType == null || !hasGenerics(receiverType)) {
return EMPTY;
}
TypeInfo rawType = receiverType.getRawType();
List<TypeParamInfo> declaredParams = rawType != null ? rawType.getTypeParams() : null;
List<TypeInfo> appliedArgs = receiverType.getAppliedTypeArgs();
Map<String, TypeInfo> applied = new HashMap<>();
if (declaredParams != null && appliedArgs != null) {
int count = Math.min(declaredParams.size(), appliedArgs.size());
for (int i = 0; i < count; i++) {
TypeParamInfo declared = declaredParams.get(i);
TypeInfo arg = appliedArgs.get(i);
if (declared == null || declared.getName() == null || declared.getName().isEmpty() || arg == null) {
continue;
}
applied.put(declared.getName(), arg);
}
}
Map<String, TypeInfo> bounds = new HashMap<>();
if (declaredParams != null) {
for (TypeParamInfo declared : declaredParams) {
if (declared == null) continue;
String name = declared.getName();
if (name == null || name.isEmpty()) continue;
TypeInfo bound = declared.getBoundTypeInfo();
if (bound != null && bound.isResolved()) {
bounds.put(name, bound);
}
}
}
return new GenericContext(applied, bounds);
}
/**
* Check if a type has generics that need substitution.
*
* Returns true if:
* - Type is parameterized (e.g., List&lt;String&gt;)
* - Type's raw form differs from the type itself
* - Type has declared type parameters
*
* @param type the type to check
* @return true if this type contains generic information that may need substitution
*/
public static boolean hasGenerics(TypeInfo type) {
return type.isParameterized() ||
type.getRawType() != type ||
(type.getTypeParams() != null && !type.getTypeParams().isEmpty());
}
/**
* Resolve a type variable to its bound or applied type.
*
* Prefers applied bindings (e.g., E -&gt; String in List&lt;String&gt;) over declared bounds.
* Falls back to bounds (e.g., T -&gt; Entity in &lt;T extends Entity&gt;) if no applied argument exists.
* Returns null if the variable is not found in either map.
*
* @param name the type variable name (e.g., "T", "E", "K")
* @return the resolved TypeInfo, or null if variable not found
*/
public TypeInfo resolveTypeVariable(String name) {
if (name == null || name.isEmpty()) {
return null;
}
TypeInfo applied = appliedBindings.get(name);
if (applied != null) {
return applied;
}
return boundFallbacks.get(name);
}
/**
* Substitute type variables in a TypeInfo recursively.
*
* Handles:
* - Direct type variables (unresolved types representing T, E, etc.)
* - Parameterized types (recursively substitutes inside type arguments)
* - Array types (substitutes the element type, preserves array dimensions)
*
* @param type the type to substitute (may contain type variables)
* @return the substituted type, or the original type if no substitutions apply
*/
public TypeInfo substitute(TypeInfo type) {
if (type == null) {
return null;
}
// Direct type variable substitution (unresolved types represent type variables like T).
if (!type.isResolved()) {
TypeInfo substitution = resolveTypeVariable(type.getSimpleName());
if (substitution != null) {
return substitution;
}
}
// Parameterized types: substitute inside args.
if (type.isParameterized()) {
List<TypeInfo> originalArgs = type.getAppliedTypeArgs();
List<TypeInfo> substitutedArgs = new ArrayList<>(originalArgs.size());
boolean changed = false;
for (TypeInfo arg : originalArgs) {
TypeInfo substitutedArg = substitute(arg);
substitutedArgs.add(substitutedArg);
if (substitutedArg != arg) {
changed = true;
}
}
if (changed) {
return type.getRawType().parameterize(substitutedArgs);
}
}
// Array wrapper types are represented by a display-name suffix; we can still substitute
// a plain "T[]" by looking up "T".
String simple = type.getSimpleName();
if (simple != null && simple.endsWith("[]")) {
TypeStringNormalizer.ArraySplit split = TypeStringNormalizer.splitArraySuffixes(simple);
String elementName = split.base;
int dims = split.dimensions;
TypeInfo elementSub = resolveTypeVariable(elementName);
if (elementSub != null) {
TypeInfo result = elementSub;
for (int i = 0; i < dims; i++) {
result = TypeInfo.arrayOf(result);
}
return result;
}
}
return type;
}
/**
* Substitute type variables in a type string.
*
* Attempts substitution in this order:
* 1. Normalize the string (strip imports, pick union branch, remove nullable suffix, split arrays)
* 2. Try direct type variable lookup (e.g., "T" -&gt; resolved Entity)
* 3. Fall back to full type resolution with substitution applied
*
* @param typeString the type expression as a string (e.g., "T", "T[]", "List&lt;T&gt;")
* @param resolver optional TypeResolver for fallback resolution; if null, returns null on failure
* @return the substituted type, or null if resolution fails
*/
public TypeInfo substituteString(String typeString, TypeResolver resolver) {
if (typeString == null || typeString.trim().isEmpty()) {
return null;
}
String normalized = TypeStringNormalizer.stripImportTypeSyntax(typeString);
normalized = TypeStringNormalizer.pickPreferredUnionBranch(normalized);
normalized = TypeStringNormalizer.stripNullableSuffix(normalized);
TypeStringNormalizer.ArraySplit arraySplit = TypeStringNormalizer.splitArraySuffixes(normalized);
String baseExpr = arraySplit.base;
int dims = arraySplit.dimensions;
String bareBase = GenericTypeParser.stripGenerics(baseExpr);
TypeInfo direct = resolveTypeVariable(bareBase);
if (direct != null) {
TypeInfo result = direct;
for (int i = 0; i < dims; i++) {
result = TypeInfo.arrayOf(result);
}
return result;
}
if (resolver == null) {
return null;
}
TypeInfo resolved = resolver.resolveJSType(typeString);
return substitute(resolved);
}
/**
* Substitute type variables in a resolved type, with string fallback.
*
* Two-phase approach:
* 1. Try substituting the resolved type directly
* 2. If that fails, attempt substitution via the raw string as fallback
*
* This handles edge cases where the TypeInfo doesn't capture enough information
* to do proper substitution (e.g., union types, complex generics).
*
* @param resolvedType a TypeInfo that has already been resolved (may still contain type variables)
* @param rawTypeString the original type string before resolution (for fallback)
* @param resolver optional TypeResolver for string-based fallback resolution
* @return the substituted type, or the best attempt if full substitution fails
*/
public TypeInfo substituteType(TypeInfo resolvedType, String rawTypeString, TypeResolver resolver) {
if (resolvedType == null) {
return null;
}
TypeInfo substituted = substitute(resolvedType);
if (substituted != null && substituted.isResolved()) {
return substituted;
}
if (rawTypeString != null && !rawTypeString.trim().isEmpty()) {
TypeInfo fromString = substituteString(rawTypeString, resolver);
if (fromString != null && fromString.isResolved()) {
return fromString;
}
}
return substituted;
}
}
@@ -0,0 +1,171 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentMap;
/**
* Adapts {@code Object}-typed method parameters to the receiver's generic type arguments
* for editor display (autocomplete/hover).
* <p>
* Java collections use {@code Object} in certain method signatures for backward-compatibility
* (e.g., {@code Map.get(Object)} instead of {@code Map.get(K)}). This adapter maps those
* {@code Object} parameters back to the appropriate generic type argument when the receiver
* is parameterized (e.g., {@code Map<String, Integer>}).
* <p>
* <b>Design rules:</b>
* <ul>
* <li>Only contract-level checks using {@code java.util.Map}, {@code java.util.Collection},
* {@code java.util.List} assignability — never concrete classes like HashMap/ArrayList.</li>
* <li>Only adapts when the reflected parameter type is exactly {@code java.lang.Object}
* AND the receiver is parameterized with applied type args.</li>
* <li>Table-driven: each rule is a {@link Rule} record in a static list.</li>
* </ul>
*/
public final class GenericParameterAdapter {
// ==================== Rule Model ====================
/**
* A single adaptation rule.
*
* @see #RULES
*/
private static final class Rule {
/** The contract interface (e.g., {@code java.util.Map}). */
final Class<?> contractType;
/** Method name to match. */
final String methodName;
/** Total parameter count the method must have. */
final int paramCount;
/** The parameter index to adapt (0-based). */
final int paramIndex;
/** Index into the receiver's applied type args (0-based). 0=K/E, 1=V, etc. */
final int typeArgIndex;
Rule(Class<?> contractType, String methodName, int paramCount, int paramIndex, int typeArgIndex) {
this.contractType = contractType;
this.methodName = methodName;
this.paramCount = paramCount;
this.paramIndex = paramIndex;
this.typeArgIndex = typeArgIndex;
}
}
// ==================== Rule Table ====================
private static final List<Rule> RULES;
static {
List<Rule> rules = new ArrayList<>();
// Map<K, V>
rules.add(new Rule(java.util.Map.class, "get", 1, 0, 0));
rules.add(new Rule(java.util.Map.class, "containsKey", 1, 0, 0));
rules.add(new Rule(java.util.Map.class, "containsValue", 1, 0, 1));
rules.add(new Rule(java.util.Map.class, "remove", 1, 0, 0));
rules.add(new Rule(java.util.Map.class, "remove", 2, 0, 0));
rules.add(new Rule(java.util.Map.class, "remove", 2, 1, 1));
rules.add(new Rule(java.util.Map.class, "getOrDefault", 2, 0, 0));
// ConcurrentMap<K, V> (get/containsKey/containsValue inherited from Map above)
rules.add(new Rule(java.util.concurrent.ConcurrentMap.class, "remove", 2, 0, 0));
rules.add(new Rule(java.util.concurrent.ConcurrentMap.class, "remove", 2, 1, 1));
rules.add(new Rule(java.util.concurrent.ConcurrentMap.class, "getOrDefault", 2, 0, 0));
// Collection<E>
rules.add(new Rule(java.util.Collection.class, "contains", 1, 0, 0));
rules.add(new Rule(java.util.Collection.class, "remove", 1, 0, 0));
// List<E> (contains/remove covered by Collection above)
rules.add(new Rule(java.util.List.class, "indexOf", 1, 0, 0));
rules.add(new Rule(java.util.List.class, "lastIndexOf", 1, 0, 0));
// Set<E> (contains/remove covered by Collection above)
// Deque<E>
rules.add(new Rule(java.util.Deque.class, "contains", 1, 0, 0));
rules.add(new Rule(java.util.Deque.class, "remove", 1, 0, 0));
rules.add(new Rule(java.util.Deque.class, "removeFirstOccurrence", 1, 0, 0));
rules.add(new Rule(java.util.Deque.class, "removeLastOccurrence", 1, 0, 0));
// BlockingQueue<E> (contains/remove covered by Collection above)
rules.add(new Rule(java.util.concurrent.BlockingQueue.class, "remove", 1, 0, 0));
rules.add(new Rule(java.util.concurrent.BlockingQueue.class, "contains", 1, 0, 0));
// BlockingDeque<E>
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "remove", 1, 0, 0));
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "contains", 1, 0, 0));
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "removeFirstOccurrence", 1, 0, 0));
rules.add(new Rule(java.util.concurrent.BlockingDeque.class, "removeLastOccurrence", 1, 0, 0));
RULES = Collections.unmodifiableList(rules);
}
// ==================== Public API ====================
/**
* Attempt to adapt a parameter type from {@code Object} to the receiver's generic type argument.
* <p>
* Returns the adapted {@link TypeInfo} if all conditions are met:
* <ol>
* <li>The receiver is parameterized and has applied type args.</li>
* <li>The parameter's current type is exactly {@code java.lang.Object}.</li>
* <li>A matching rule exists in the rule table.</li>
* <li>The rule's required type arg index is within bounds.</li>
* </ol>
* Otherwise returns {@code null} (meaning: keep the original type).
*
* @param method the reflected Java method
* @param receiverType the parameterized receiver TypeInfo (e.g., {@code Map<String, Integer>})
* @param paramIndex the index of the parameter being checked
* @param currentType the current TypeInfo for that parameter (after normal substitution)
* @return adapted TypeInfo, or {@code null} if no adaptation applies
*/
public static TypeInfo adaptParameterType(Method method, TypeInfo receiverType,
int paramIndex, TypeInfo currentType) {
// Guard: only adapt Object params on parameterized receivers
if (receiverType == null || !receiverType.isParameterized()) {
return null;
}
if (currentType == null || currentType.getJavaClass() != Object.class) {
return null;
}
List<TypeInfo> appliedArgs = receiverType.getAppliedTypeArgs();
if (appliedArgs == null || appliedArgs.isEmpty()) {
return null;
}
// Determine the declaring class of the method
Class<?> declaringClass = method.getDeclaringClass();
String methodName = method.getName();
int paramCount = method.getParameterCount();
// Scan rules for a match
for (Rule rule : RULES) {
if (rule.paramIndex != paramIndex) continue;
if (rule.paramCount != paramCount) continue;
if (!rule.methodName.equals(methodName)) continue;
if (!rule.contractType.isAssignableFrom(declaringClass)) continue;
// Rule matches — check if the type arg index is within bounds
if (rule.typeArgIndex < appliedArgs.size()) {
TypeInfo adapted = appliedArgs.get(rule.typeArgIndex);
if (adapted != null && adapted.isResolved()) {
return adapted;
}
}
}
return null;
}
private GenericParameterAdapter() {
// Utility class — no instantiation
}
}
@@ -0,0 +1,325 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import java.util.ArrayList;
import java.util.List;
/**
* Parser for generic type expressions with nested type arguments.
*
* Handles type expressions like:
* - Simple types: "String", "IPlayer"
* - Qualified names: "java.util.List", "IPlayerEvent.InteractEvent"
* - Single generic: "List<String>", "Consumer<IAction>"
* - Multiple generics: "Map<String, Integer>"
* - Nested generics: "List<Map<String, Integer>>"
*
* Note: union/nullability/array semantics are handled by higher-level resolvers.
* This parser only consumes suffix tokens where needed to correctly delimit type arguments.
*
* This parser produces a ParsedType tree that can be resolved into parameterized TypeInfo instances.
*/
public class GenericTypeParser {
/**
* Represents a parsed type expression with potential generic arguments.
*/
public static class ParsedType {
/** The base type name (may be qualified, e.g., "java.util.List" or "IPlayer") */
public final String baseName;
/** The applied type arguments (for generics like List<String>), each is itself a ParsedType */
public final List<ParsedType> typeArgs;
/** The original raw string before parsing */
public final String rawString;
public ParsedType(String baseName, List<ParsedType> typeArgs, String rawString) {
this.baseName = baseName;
this.typeArgs = typeArgs != null ? typeArgs : new ArrayList<>();
this.rawString = rawString;
}
/**
* Check if this parsed type has generic arguments.
*/
public boolean hasTypeArgs() {
return !typeArgs.isEmpty();
}
/**
* Get display string for debugging.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder(baseName);
if (!typeArgs.isEmpty()) {
sb.append("<");
for (int i = 0; i < typeArgs.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(typeArgs.get(i).toString());
}
sb.append(">");
}
return sb.toString();
}
}
/**
* Parse a type expression string into a ParsedType tree.
*
* @param typeExpr The type expression to parse (e.g., "List<Map<String, Integer>>[]")
* @return The parsed type, or null if parsing fails
*/
public static ParsedType parse(String typeExpr) {
if (typeExpr == null || typeExpr.isEmpty()) {
return null;
}
String expr = typeExpr.trim();
// Now parse the base type with potential generic arguments
ParseResult result = parseTypeWithGenerics(expr, 0);
if (result == null || result.type == null) {
// Fallback: treat entire expression as a simple type
return new ParsedType(expr, null, typeExpr);
}
return result.type;
}
/**
* Internal result class for recursive parsing.
*/
private static class ParseResult {
final ParsedType type;
final int endIndex; // position after this type in the string
ParseResult(ParsedType type, int endIndex) {
this.type = type;
this.endIndex = endIndex;
}
}
/**
* Parse a type expression starting at the given index, handling nested generics.
* Returns the parsed type and the index after this type expression.
*/
private static ParseResult parseTypeWithGenerics(String expr, int startIndex) {
int i = startIndex;
int len = expr.length();
// Skip leading whitespace
while (i < len && Character.isWhitespace(expr.charAt(i))) {
i++;
}
if (i >= len) {
return null;
}
// Read the base type name (can include dots for qualified names)
StringBuilder baseNameBuilder = new StringBuilder();
while (i < len) {
char c = expr.charAt(i);
if (Character.isJavaIdentifierPart(c) || c == '.') {
baseNameBuilder.append(c);
i++;
} else {
break;
}
}
String baseName = baseNameBuilder.toString().trim();
if (baseName.isEmpty()) {
return null;
}
// Remove trailing dots if any
while (baseName.endsWith(".")) {
baseName = baseName.substring(0, baseName.length() - 1);
}
// Skip whitespace before potential <
while (i < len && Character.isWhitespace(expr.charAt(i))) {
i++;
}
// Check for generic arguments
List<ParsedType> typeArgs = new ArrayList<>();
if (i < len && expr.charAt(i) == '<') {
i++; // Skip '<'
// Parse type arguments (comma-separated, handling nested <>)
while (i < len) {
// Skip whitespace
while (i < len && Character.isWhitespace(expr.charAt(i))) {
i++;
}
if (i >= len) break;
char c = expr.charAt(i);
if (c == '>') {
i++; // Skip '>'
break;
}
if (c == ',') {
i++; // Skip ','
continue;
}
// Parse a type argument (may itself have generics)
ParseResult argResult = parseTypeArgument(expr, i);
if (argResult != null && argResult.type != null) {
typeArgs.add(argResult.type);
i = argResult.endIndex;
} else {
// Skip forward until the next ',' or '>' at the current nesting level.
i = skipToNextTypeArgDelimiter(expr, i);
}
}
}
ParsedType type = new ParsedType(baseName, typeArgs, expr.substring(startIndex, i));
return new ParseResult(type, i);
}
/**
* Skip forward until we hit ',' or '>' that would delimit a type argument.
* This prevents producing spurious additional type args when encountering unsupported syntax.
*/
private static int skipToNextTypeArgDelimiter(String expr, int startIndex) {
int i = startIndex;
int depth = 0;
boolean inString = false;
char stringChar = 0;
while (i < expr.length()) {
char c = expr.charAt(i);
if (inString) {
if (c == stringChar && (i == 0 || expr.charAt(i - 1) != '\\')) {
inString = false;
}
i++;
continue;
}
if (c == '\'' || c == '"') {
inString = true;
stringChar = c;
i++;
continue;
}
if (c == '<' || c == '(' || c == '[') {
depth++;
} else if (c == '>' || c == ')' || c == ']') {
if (depth > 0) {
depth--;
} else if (c == '>') {
return i; // Let caller handle closing '>'
}
} else if ((c == ',' || c == '>') && depth == 0) {
return i;
}
i++;
}
return i;
}
/**
* Parse a single type argument, which may include nested generics and array suffixes.
*/
private static ParseResult parseTypeArgument(String expr, int startIndex) {
int i = startIndex;
int len = expr.length();
// Skip leading whitespace
while (i < len && Character.isWhitespace(expr.charAt(i))) {
i++;
}
if (i >= len) {
return null;
}
// Check for wildcard (? extends X, ? super X, or just ?)
if (expr.charAt(i) == '?') {
i++;
// Skip whitespace
while (i < len && Character.isWhitespace(expr.charAt(i))) {
i++;
}
// Check for "extends" or "super"
if (i < len && expr.substring(i).startsWith("extends ")) {
i += 8; // Skip "extends "
// Parse the bound type
ParseResult boundResult = parseTypeWithGenerics(expr, i);
if (boundResult != null) {
return boundResult;
}
} else if (i < len && expr.substring(i).startsWith("super ")) {
i += 6; // Skip "super "
// Parse the bound type
ParseResult boundResult = parseTypeWithGenerics(expr, i);
if (boundResult != null) {
return boundResult;
}
}
// Plain wildcard, treat as Object
return new ParseResult(new ParsedType("Object", null, "?"), i);
}
// Parse regular type with potential generics
ParseResult typeResult = parseTypeWithGenerics(expr, i);
if (typeResult == null) {
return null;
}
i = typeResult.endIndex;
ParsedType baseType = typeResult.type;
// Skip whitespace
while (i < len && Character.isWhitespace(expr.charAt(i))) {
i++;
}
// Consume array suffixes / nullable suffix so the caller can correctly find delimiters.
while (i + 1 < len && expr.charAt(i) == '[' && expr.charAt(i + 1) == ']') {
i += 2;
while (i < len && Character.isWhitespace(expr.charAt(i))) {
i++;
}
}
if (i < len && expr.charAt(i) == '?') {
i++;
}
// Preserve the full raw substring (including suffixes) for higher-level resolvers.
if (i != typeResult.endIndex) {
return new ParseResult(new ParsedType(baseType.baseName, baseType.typeArgs, expr.substring(startIndex, i)), i);
}
return new ParseResult(baseType, i);
}
/**
* Convenience method to check if a type string contains generic arguments.
*/
public static boolean hasGenerics(String typeExpr) {
return typeExpr != null && typeExpr.contains("<") && typeExpr.contains(">");
}
/**
* Strip generic arguments from a type string (for base type lookup).
* Example: "List<String>" -> "List"
*/
public static String stripGenerics(String typeExpr) {
if (typeExpr == null) return null;
int idx = typeExpr.indexOf('<');
if (idx > 0) {
return typeExpr.substring(0, idx).trim();
}
return typeExpr;
}
}
@@ -0,0 +1,183 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents a single import statement with all its metadata.
* Tracks the full import path, resolution status, source positions, and references.
*/
public final class ImportData {
private final String fullPath; // e.g., "java.util.List" or "kamkeel.npcdbc.api.*"
private final String simpleName; // e.g., "List" or null for wildcard
private final boolean isWildcard; // true if ends with .*
private final boolean isStatic; // true if 'import static'
private final int startOffset; // start of 'import' keyword in source
private final int endOffset; // end of import statement (before or at semicolon)
private final int pathStartOffset; // start of the package.Class path
private final int pathEndOffset; // end of the package.Class path (before .*)
private TypeInfo resolvedType; // null if wildcard or unresolved
private boolean resolved; // whether resolution was attempted and succeeded
// 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) {
this.fullPath = fullPath;
this.simpleName = simpleName;
this.isWildcard = isWildcard;
this.isStatic = isStatic;
this.startOffset = startOffset;
this.endOffset = endOffset;
this.pathStartOffset = pathStartOffset;
this.pathEndOffset = pathEndOffset;
this.resolved = false;
}
// Getters
public String getFullPath() { return fullPath; }
public String getSimpleName() { return simpleName; }
public boolean isWildcard() { return isWildcard; }
public boolean isStatic() { return isStatic; }
public int getStartOffset() { return startOffset; }
public int getEndOffset() { return endOffset; }
public int getPathStartOffset() { return pathStartOffset; }
public int getPathEndOffset() { return pathEndOffset; }
public TypeInfo getResolvedType() { return resolvedType; }
public boolean isResolved() { return resolved; }
/**
* Set the resolved type information.
*/
public void setResolvedType(TypeInfo typeInfo) {
this.resolvedType = typeInfo;
this.resolved = typeInfo != null && typeInfo.isResolved();
}
/**
* Mark this import as resolved (for wildcard imports where we confirm the package exists).
*/
public void markResolved(boolean resolved) {
this.resolved = resolved;
}
// ==================== 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.
* For "java.util.List" returns "java.util"
* For wildcards like "java.util.*" returns "java.util"
*/
public String getPackagePortion() {
if (isWildcard) {
return fullPath;
}
int lastDot = fullPath.lastIndexOf('.');
return lastDot > 0 ? fullPath.substring(0, lastDot) : "";
}
/**
* Get all segments of the import path.
*/
public String[] getPathSegments() {
return fullPath.split("\\.");
}
/**
* Check if this import could provide the given simple class name.
* For wildcard imports, always returns true (might provide any class).
* For specific imports, checks if the simple name matches.
*/
public boolean couldProvide(String className) {
if (isWildcard) {
return true;
}
return className.equals(simpleName);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("import ");
if (isStatic) sb.append("static ");
sb.append(fullPath);
if (isWildcard) sb.append(".*");
sb.append(" [").append(resolved ? "resolved" : "unresolved").append("]");
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImportData that = (ImportData) o;
return fullPath.equals(that.fullPath) && isWildcard == that.isWildcard && isStatic == that.isStatic;
}
@Override
public int hashCode() {
return fullPath.hashCode() * 31 + (isWildcard ? 1 : 0) + (isStatic ? 2 : 0);
}
}
@@ -0,0 +1,282 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticMethod;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticParameter;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticTypeBuilder;
import noppes.npcs.client.gui.util.script.interpreter.type.synthetic.SyntheticType;
import java.util.*;
/**
* Registry for Nashorn built-in types and global functions.
*
* <p>Nashorn provides several built-in objects and functions:</p>
* <ul>
* <li><b>Java</b> - Object for Java interop: type(), extend(), from(), to()</li>
* <li><b>Packages</b> - Root of package hierarchy for accessing Java packages</li>
* <li><b>JavaImporter</b> - Creates scope with imported Java packages</li>
* <li><b>print()</b> - Global print function</li>
* <li><b>load()</b> - Load and execute a script</li>
* </ul>
*/
public class NashornBuiltins {
private static NashornBuiltins instance;
private final Map<String, SyntheticType> builtinTypes = new LinkedHashMap<>();
private final Map<String, SyntheticMethod> globalFunctions = new LinkedHashMap<>();
private NashornBuiltins() {
initializeBuiltins();
}
public static NashornBuiltins getInstance() {
if (instance == null) {
instance = new NashornBuiltins();
}
return instance;
}
private void initializeBuiltins() {
// ==================== Java object ====================
SyntheticType javaType = new SyntheticTypeBuilder("Java")
.documentation("Nashorn's Java interop object for accessing Java types and utilities.")
.addMethod("type")
.parameter("className", "String", "Fully-qualified Java class name")
.returns("java.lang.Class")
.returnsResolved(args -> {
if (args != null && args.length > 0 && args[0] != null) {
String className = args[0];
// Remove quotes if present
if (className.startsWith("\"") && className.endsWith("\"")) {
className = className.substring(1, className.length() - 1);
} else if (className.startsWith("'") && className.endsWith("'")) {
className = className.substring(1, className.length() - 1);
}
// Try to load the class
TypeInfo resolved = TypeResolver.getInstance().resolve(className);
if (resolved != null && resolved.isResolved()) {
return new ClassTypeInfo(resolved);
}
}
return null;
})
.documentation("Loads a Java class by its fully-qualified name.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"var File = Java.type(\"java.io.File\");\n" +
"var file = new File(\"path/to/file\");\n" +
"```\n\n" +
"@param className The fully-qualified Java class name\n" +
"@returns The Java class reference (use with 'new' to create instances)")
.done()
.addMethod("extend")
.parameter("type", "java.lang.Class", "Java class or interface to extend/implement")
.returns("java.lang.Class")
.documentation("Creates a subclass or implementation of a Java class or interface.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"var MyRunnable = Java.extend(Java.type(\"java.lang.Runnable\"), {\n" +
" run: function() {\n" +
" print(\"Running!\");\n" +
" }\n" +
"});\n" +
"```\n\n" +
"@param type The Java class or interface to extend\n" +
"@returns A new class that extends/implements the given type")
.done()
.addMethod("from")
.parameter("javaArray", "Object", "A Java array or Collection")
.returns("Object[]")
.documentation("Converts a Java array or Collection to a JavaScript array.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"var jsList = Java.from(javaArrayList);\n" +
"jsList.forEach(function(item) { print(item); });\n" +
"```\n\n" +
"@param javaArray A Java array or java.util.Collection\n" +
"@returns A JavaScript array containing the elements")
.done()
.addMethod("to")
.parameter("jsArray", "Object[]", "A JavaScript array")
.parameter("javaType", "Class", "The target Java array type")
.returns("Object")
.documentation("Converts a JavaScript array to a Java array of the specified type.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"var jsArray = [1, 2, 3];\n" +
"var intArray = Java.to(jsArray, \"int[]\");\n" +
"```\n\n" +
"@param jsArray A JavaScript array\n" +
"@param javaType The target Java array type (e.g., \"int[]\", \"java.lang.String[]\")\n" +
"@returns A Java array of the specified type")
.done()
.addMethod("super")
.parameter("object", "Object", "A Java object created via Java.extend()")
.returns("Object")
.documentation("Gets a reference to the super class for calling super methods.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"var MyList = Java.extend(Java.type(\"java.util.ArrayList\"), {\n" +
" add: function(e) {\n" +
" print(\"Adding: \" + e);\n" +
" return Java.super(this).add(e);\n" +
" }\n" +
"});\n" +
"```\n\n" +
"@param object An extended Java object\n" +
"@returns A reference to call super methods")
.done()
.addMethod("synchronized")
.parameter("func", "java.util.function.Function", "The function to synchronize")
.parameter("lock", "Object", "The object to synchronize on")
.returns("java.util.function.Function")
.documentation("Wraps a function to execute synchronized on a given object.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"var syncFunc = Java.synchronized(function() {\n" +
" // Thread-safe code here\n" +
"}, lockObject);\n" +
"```\n\n" +
"@param func The function to synchronize\n" +
"@param lock The object to use as the monitor\n" +
"@returns A synchronized version of the function")
.done()
.build();
builtinTypes.put("Java", javaType);
// ==================== Global print function ====================
SyntheticType printFunc = new SyntheticTypeBuilder("print")
.addMethod("print")
.parameter("message", "Object", "The message to print")
.returns("void")
.documentation("Prints a message to standard output.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"print(\"Hello, world!\");\n" +
"print(myObject);\n" +
"```")
.done()
.build();
globalFunctions.put("print", printFunc.getMethod("print"));
// ==================== Global load function ====================
SyntheticType loadFunc = new SyntheticTypeBuilder("load")
.addMethod("load")
.parameter("script", "String", "Path or URL to the script")
.returns("Object")
.documentation("Loads and executes a script file or URL.\n\n" +
"**Usage:**\n" +
"```javascript\n" +
"load(\"./myScript.js\");\n" +
"load(\"http://example.com/script.js\");\n" +
"```\n\n" +
"@param script Path to a local script or URL\n" +
"@returns The result of the script execution")
.done()
.build();
globalFunctions.put("load", loadFunc.getMethod("load"));
}
/**
* Get a built-in type by name.
* @param name The type name (e.g., "Java")
* @return The SyntheticType or null if not found
*/
public SyntheticType getBuiltinType(String name) {
return builtinTypes.get(name);
}
/**
* Check if a name is a built-in type.
*/
public boolean isBuiltinType(String name) {
return builtinTypes.containsKey(name);
}
/**
* Get a global function by name.
*/
public SyntheticMethod getGlobalFunction(String name) {
return globalFunctions.get(name);
}
/**
* Check if a name is a global function.
*/
public boolean isGlobalFunction(String name) {
return globalFunctions.containsKey(name);
}
/**
* Get all built-in type names.
*/
public Set<String> getBuiltinTypeNames() {
return Collections.unmodifiableSet(builtinTypes.keySet());
}
/**
* Get all global function names.
*/
public Set<String> getGlobalFunctionNames() {
return Collections.unmodifiableSet(globalFunctions.keySet());
}
/**
* Get all built-in types.
*/
public Collection<SyntheticType> getAllBuiltinTypes() {
return Collections.unmodifiableCollection(builtinTypes.values());
}
/**
* Get all global functions as SyntheticTypes.
* Each function is wrapped in its own SyntheticType for the registry.
*/
public Map<String, SyntheticType> getAllGlobalFunctions() {
Map<String, SyntheticType> result = new LinkedHashMap<>();
// Wrap each global function in a minimal SyntheticType
for (Map.Entry<String, SyntheticMethod> entry : globalFunctions.entrySet()) {
String name = entry.getKey();
SyntheticMethod method = entry.getValue();
// Create a synthetic type that just contains this one global function
SyntheticTypeBuilder builder = new SyntheticTypeBuilder(name);
builder.documentation("Global " + name + " function");
// Add the method
SyntheticTypeBuilder.MethodBuilder methodBuilder = builder.addMethod(name);
for (SyntheticParameter param : method.parameters) {
methodBuilder.parameter(param.name, param.typeName, param.documentation);
}
methodBuilder.returns(method.returnType);
if (method.documentation != null && !method.documentation.isEmpty()) {
methodBuilder.documentation(method.documentation);
}
methodBuilder.done();
result.put(name, builder.build());
}
return result;
}
/**
* Resolve the return type of Java.type() given the class name argument.
*/
public TypeInfo resolveJavaType(String className) {
SyntheticType javaBuiltin = builtinTypes.get("Java");
if (javaBuiltin != null) {
return javaBuiltin.resolveMethodReturnType("type", new String[]{className});
}
return null;
}
}
@@ -0,0 +1,319 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import java.util.List;
/**
* Helper class for selecting the best method overload from a list of candidates.
*
* This encapsulates the scoring-based overload selection logic that considers:
* - Exact type matches
* - Numeric promotion compatibility
* - Arity (parameter count) matching
* - Varargs handling
* - Unknown (null) argument types
*
* Lower scores indicate better matches.
*/
public class OverloadSelector {
// Scoring constants
private static final int ARITY_MISMATCH_BASE = 10000;
private static final int VARARGS_PENALTY = 50;
private static final int UNKNOWN_TYPE_PENALTY = 10;
private static final int COMPATIBLE_TYPE_PENALTY = 5;
private static final int INCOMPATIBLE_TYPE_PENALTY = 100;
/**
* Select the best overload from a list of candidates based on argument types.
*
* Selection phases:
* 1. Exact match (all types match exactly)
* 2. Numeric promotion match (all args can promote to common param type)
* 3. Scoring-based selection (handles unknown types, varargs, etc.)
*
* @param overloads List of candidate method overloads
* @param argTypes Array of argument types (may contain nulls for unknown types)
* @return The best matching overload, or the first overload if no good match found
*/
public static MethodInfo selectBestOverload(List<MethodInfo> overloads, TypeInfo[] argTypes) {
if (overloads == null || overloads.isEmpty()) return null;
int argCount = (argTypes == null) ? 0 : argTypes.length;
// Fast path: If no arguments provided, try to find zero-arg method
if (argCount == 0) {
for (MethodInfo method : overloads) {
if (method.getParameterCount() == 0) {
return method;
}
}
// Fall back to closest arity
return selectClosestArity(overloads, 0);
}
// Check if all arg types are known (non-null)
boolean allArgsKnown = true;
for (TypeInfo argType : argTypes) {
if (argType == null) {
allArgsKnown = false;
break;
}
}
// Phase 1: Try exact match (only when all args known)
if (allArgsKnown) {
MethodInfo exactMatch = findExactMatch(overloads, argTypes, argCount);
if (exactMatch != null) return exactMatch;
}
// Phase 2: Try numeric promotion match (only when all args known)
if (allArgsKnown) {
MethodInfo numericMatch = findNumericPromotionMatch(overloads, argTypes, argCount);
if (numericMatch != null) return numericMatch;
}
// Phase 3: Scoring-based selection with arity-first preference
// This handles unknown (null) arg types gracefully and prefers fixed-arity over varargs
return scoringBasedSelection(overloads, argTypes, argCount);
}
/**
* Find an overload with exact type match for all arguments.
*/
private static MethodInfo findExactMatch(List<MethodInfo> overloads, TypeInfo[] argTypes, int argCount) {
for (MethodInfo method : overloads) {
if (method.getParameterCount() == argCount) {
boolean exactMatch = true;
List<FieldInfo> params = method.getParameters();
for (int i = 0; i < argCount; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
TypeInfo argType = argTypes[i];
if (paramType == null || !paramType.equals(argType)) {
exactMatch = false;
break;
}
}
if (exactMatch) return method;
}
}
return null;
}
/**
* Find an overload where all numeric arguments can promote to a common parameter type.
* Prefers narrower numeric types (e.g., int over long over double).
*/
private static MethodInfo findNumericPromotionMatch(List<MethodInfo> overloads, TypeInfo[] argTypes, int argCount) {
MethodInfo bestNumericMatch = null;
int bestNumericRank = Integer.MAX_VALUE;
for (MethodInfo method : overloads) {
if (method.getParameterCount() == argCount) {
List<FieldInfo> params = method.getParameters();
// Check if all parameters and arguments are numeric primitives
boolean allNumeric = true;
for (int i = 0; i < argCount; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
TypeInfo argType = argTypes[i];
if (paramType == null ||
!TypeChecker.isNumericPrimitive(paramType) || !TypeChecker.isNumericPrimitive(argType)) {
allNumeric = false;
break;
}
}
if (allNumeric) {
// Check if all parameters are the same numeric type
TypeInfo commonParamType = params.get(0).getTypeInfo();
boolean allParamsSame = true;
for (int i = 1; i < params.size(); i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
if (!paramType.equals(commonParamType)) {
allParamsSame = false;
break;
}
}
// If all parameters are the same numeric type, check if args can promote to it
if (allParamsSame) {
boolean canPromote = true;
for (int i = 0; i < argCount; i++) {
if (!TypeChecker.canPromoteNumeric(argTypes[i], commonParamType)) {
canPromote = false;
break;
}
}
// If all args can promote, check if this is the narrowest match so far
if (canPromote) {
int paramRank = TypeChecker.getNumericRank(commonParamType.getJavaClass());
if (paramRank < bestNumericRank) {
bestNumericRank = paramRank;
bestNumericMatch = method;
}
}
}
}
}
}
return bestNumericMatch;
}
/**
* Use scoring to select the best overload when exact/numeric matches don't apply.
*/
private static MethodInfo scoringBasedSelection(List<MethodInfo> overloads, TypeInfo[] argTypes, int argCount) {
MethodInfo bestCandidate = null;
int bestScore = Integer.MAX_VALUE;
for (MethodInfo method : overloads) {
int score = scoreOverload(method, argTypes, argCount);
if (score < bestScore) {
bestScore = score;
bestCandidate = method;
}
}
return (bestCandidate != null) ? bestCandidate : overloads.get(0);
}
/**
* Score an overload for matching against provided arguments.
* Lower score = better match.
*
* Scoring priorities:
* - Arity mismatch: +10000 + |paramCount - argCount|
* - Varargs penalty: +1000
* - Unknown arg type (null): +10 per arg
* - Compatible but not exact: +5 per arg
* - Incompatible type: +100 per arg
* - Exact type match: +0 per arg
*
* @param method The method overload to score
* @param argTypes Array of argument types (may contain nulls)
* @param argCount Number of arguments
* @return Score (lower is better)
*/
public static int scoreOverload(MethodInfo method, TypeInfo[] argTypes, int argCount) {
int score = 0;
int paramCount = method.getParameterCount();
// Check if method is varargs
boolean isVarArgs = false;
java.lang.reflect.Method javaMethod = method.getJavaMethod();
if (javaMethod != null) {
isVarArgs = javaMethod.isVarArgs();
} else {
// JS methods from .d.ts have no backing Java method; vararg info lives on the last FieldInfo
List<FieldInfo> checkParams = method.getParameters();
if (!checkParams.isEmpty()) {
isVarArgs = checkParams.get(checkParams.size() - 1).isVarArg();
}
}
// Determine arity applicability
boolean arityApplicable;
if (isVarArgs) {
// Varargs methods accept argCount >= paramCount - 1
// (the varargs array can be empty or have multiple elements)
arityApplicable = argCount >= paramCount - 1;
} else {
arityApplicable = paramCount == argCount;
}
if (!arityApplicable) {
// Large penalty for arity mismatch, plus distance to encourage closest match
score += ARITY_MISMATCH_BASE + Math.abs(paramCount - argCount);
}
// Varargs penalty (fixed-arity preferred over varargs when both apply)
if (isVarArgs) {
score += VARARGS_PENALTY;
}
// Score each argument's type compatibility
List<FieldInfo> params = method.getParameters();
int paramsToCheck = Math.min(argCount, paramCount);
for (int i = 0; i < paramsToCheck; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
TypeInfo argType = (argTypes != null && i < argTypes.length) ? argTypes[i] : null;
if (argType == null) {
// Unknown arg type - don't disqualify, but add penalty
score += UNKNOWN_TYPE_PENALTY;
} else if (paramType == null) {
// Unknown param type - add penalty
score += UNKNOWN_TYPE_PENALTY;
} else if (paramType.equals(argType)) {
// Exact match - no penalty
score += 0;
} else if (TypeChecker.isTypeCompatible(paramType, argType)) {
// Compatible but not exact
score += COMPATIBLE_TYPE_PENALTY;
} else {
// Incompatible type
score += INCOMPATIBLE_TYPE_PENALTY;
}
}
// For vararg methods, also score extra args (index >= paramCount) against the element type
if (isVarArgs && argCount > paramCount && !params.isEmpty()) {
TypeInfo varargElemType = params.get(paramCount - 1).getTypeInfo();
for (int i = paramCount; i < argCount; i++) {
TypeInfo argType = (argTypes != null && i < argTypes.length) ? argTypes[i] : null;
if (argType == null || varargElemType == null) {
score += UNKNOWN_TYPE_PENALTY;
} else if (varargElemType.equals(argType)) {
// exact match
} else if (TypeChecker.isTypeCompatible(varargElemType, argType)) {
score += COMPATIBLE_TYPE_PENALTY;
} else {
score += INCOMPATIBLE_TYPE_PENALTY;
}
}
}
// Penalize extra args that don't match params (for non-varargs)
if (!isVarArgs && argCount > paramCount) {
score += (argCount - paramCount) * INCOMPATIBLE_TYPE_PENALTY;
}
return score;
}
/**
* Select the overload with closest arity to the target.
* Tie-breaker: lower parameter count wins.
*
* @param overloads List of candidate overloads
* @param targetArity The desired number of parameters
* @return The overload with closest arity, or first overload if list is empty
*/
public static MethodInfo selectClosestArity(List<MethodInfo> overloads, int targetArity) {
if (overloads == null || overloads.isEmpty()) return null;
MethodInfo best = null;
int bestDistance = Integer.MAX_VALUE;
int bestParamCount = Integer.MAX_VALUE;
for (MethodInfo method : overloads) {
int paramCount = method.getParameterCount();
int distance = Math.abs(paramCount - targetArity);
// Prefer smaller distance; tie-break by lower paramCount
if (distance < bestDistance || (distance == bestDistance && paramCount < bestParamCount)) {
best = method;
bestDistance = distance;
bestParamCount = paramCount;
}
}
return (best != null) ? best : overloads.get(0);
}
}
@@ -0,0 +1,945 @@
package noppes.npcs.client.gui.util.script.interpreter.type;
import noppes.npcs.client.gui.util.script.interpreter.field.EnumConstantInfo;
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
import noppes.npcs.client.gui.util.script.interpreter.method.MethodSignature;
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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;
private final int modifiers; // Java reflection modifiers (public, static, final, etc.)
// 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<MethodInfo> constructors = new ArrayList<>(); // List of constructors
private final List<ScriptTypeInfo> innerClasses = new ArrayList<>();
// Enum constants (for enum types only) - name -> EnumConstantInfo
private final Map<String, EnumConstantInfo> enumConstants = new HashMap<>();
// 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<TypeInfo> 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<String> 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);
this.scriptClassName = simpleName;
this.declarationOffset = declarationOffset;
this.bodyStart = bodyStart;
this.bodyEnd = bodyEnd;
this.modifiers = modifiers;
}
// Factory method
public static ScriptTypeInfo create(String simpleName, Kind kind,
int declarationOffset, int bodyStart, int bodyEnd, int modifiers) {
return new ScriptTypeInfo(simpleName, simpleName, kind, declarationOffset, bodyStart, bodyEnd, modifiers);
}
public static ScriptTypeInfo createInner(String simpleName, Kind kind, ScriptTypeInfo outer,
int declarationOffset, int bodyStart, int bodyEnd, int modifiers) {
String fullName = outer.getFullName() + "$" + simpleName;
ScriptTypeInfo inner = new ScriptTypeInfo(simpleName, fullName, kind, declarationOffset, bodyStart, bodyEnd, modifiers);
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 int getModifiers() { return modifiers; }
public ScriptTypeInfo getOuterClass() { return outerClass; }
public List<ScriptTypeInfo> 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<TypeInfo> getImplementedInterfaces() {
return new ArrayList<>(implementedInterfaces);
}
/**
* Get the raw interface names as written in the script.
* @return Unmodifiable list of interface name strings
*/
public List<String> 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.
*/
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) {
if (fields.containsKey(fieldName))
return true;
if (enumConstants.containsKey(fieldName))
return true;
return false;
}
@Override
public FieldInfo getFieldInfo(String fieldName) {
if (fields.containsKey(fieldName))
return fields.get(fieldName);
if (enumConstants.containsKey(fieldName)) {
EnumConstantInfo enumConst = enumConstants.get(fieldName);
if (enumConst != null)
return enumConst.getFieldInfo();
}
return null;
}
public Map<String, FieldInfo> getFields() {
return new HashMap<>(fields);
}
// ==================== ENUM CONSTANT MANAGEMENT ====================
/**
* Add an enum constant to this enum type.
* Only valid for enum types.
*/
public void addEnumConstant(EnumConstantInfo constant) {
if (isEnum()) {
enumConstants.put(constant.getFieldInfo().getName(), constant);
}
}
/**
* Check if this enum has a constant with the given name.
*/
public boolean hasEnumConstant(String constantName) {
return enumConstants.containsKey(constantName);
}
/**
* Get an enum constant by name.
*/
public EnumConstantInfo getEnumConstant(String constantName) {
return enumConstants.get(constantName);
}
/**
* Get all enum constants.
*/
public Map<String, EnumConstantInfo> getEnumConstants() {
return new HashMap<>(enumConstants);
}
/**
* Check if this is an enum type and has any constants.
*/
public boolean hasEnumConstants() {
return isEnum() && !enumConstants.isEmpty();
}
// ==================== METHOD MANAGEMENT ====================
public void addMethod(MethodInfo method) {
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> getAllMethodOverloads(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);
}
public List<MethodInfo> getAllMethodsFlat() {
List<MethodInfo> allMethods = new ArrayList<>();
for (List<MethodInfo> overloads : methods.values()) {
allMethods.addAll(overloads);
}
return allMethods;
}
// ==================== CONSTRUCTOR MANAGEMENT ====================
public void addConstructor(MethodInfo constructor) {
constructors.add(constructor);
}
@Override
public List<MethodInfo> getConstructors() {
return new ArrayList<>(constructors);
}
@Override
public boolean hasConstructors() {
return !constructors.isEmpty();
}
/**
* Find the best matching constructor for the given argument count.
* Returns null if no constructor matches.
*/
@Override
public MethodInfo findConstructor(int argCount) {
for (MethodInfo constructor : constructors) {
if (constructor.getParameterCount() == argCount) {
return constructor;
}
}
return null;
}
public MethodInfo findConstructor(TypeInfo[] argTypes) {
for (MethodInfo constructor : constructors) {
if (constructor.getParameterCount() == argTypes.length) {
boolean match = true;
List<FieldInfo> params = constructor.getParameters();
for (int i = 0; i < argTypes.length; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
if (!TypeChecker.isTypeCompatible(paramType, argTypes[i])) {
match = false;
break;
}
}
if (match)
return constructor;
}
}
return null;
}
// ==================== INHERITANCE HIERARCHY SEARCH ====================
/**
* Check if this type or any of its parent classes has a field with the given name.
* This recursively searches up the inheritance tree.
*/
public boolean hasFieldInHierarchy(String fieldName) {
// Check this class first
if (hasField(fieldName)) {
return true;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).hasFieldInHierarchy(fieldName);
} else {
// For Java classes, hasField already checks inheritance via reflection
return superClass.hasField(fieldName);
}
}
return false;
}
/**
* Get field info from this type or any of its parent classes.
* This recursively searches up the inheritance tree.
*/
public FieldInfo getFieldInfoInHierarchy(String fieldName) {
// Check this class first
FieldInfo field = getFieldInfo(fieldName);
if (field != null) {
return field;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).getFieldInfoInHierarchy(fieldName);
} else {
// For Java classes, getFieldInfo already checks inheritance via reflection
return superClass.getFieldInfo(fieldName);
}
}
return null;
}
/**
* Check if this type or any of its parent classes has a method with the given name.
* This recursively searches up the inheritance tree.
*/
public boolean hasMethodInHierarchy(String methodName) {
// Check this class first
if (hasMethod(methodName)) {
return true;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName);
} else {
// For Java classes, hasMethod already checks inheritance via reflection
return superClass.hasMethod(methodName);
}
}
return false;
}
/**
* Check if this type or any of its parent classes has a method with the given name and parameter count.
* This recursively searches up the inheritance tree.
*/
public boolean hasMethodInHierarchy(String methodName, int paramCount) {
// Check this class first
if (hasMethod(methodName, paramCount)) {
return true;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).hasMethodInHierarchy(methodName, paramCount);
} else {
// For Java classes, hasMethod already checks inheritance via reflection
return superClass.hasMethod(methodName, paramCount);
}
}
return false;
}
/**
* Get method info from this type or any of its parent classes.
* This recursively searches up the inheritance tree.
*/
public MethodInfo getMethodInfoInHierarchy(String methodName) {
// Check this class first
MethodInfo method = getMethodInfo(methodName);
if (method != null) {
return method;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).getMethodInfoInHierarchy(methodName);
} else {
// For Java classes, getMethodInfo already checks inheritance via reflection
return superClass.getMethodInfo(methodName);
}
}
return null;
}
/**
* Get method info with specific parameter count from this type or any of its parent classes.
* This recursively searches up the inheritance tree.
*/
public MethodInfo getMethodWithParamCountInHierarchy(String methodName, int paramCount) {
// Check this class first
MethodInfo method = getMethodWithParamCount(methodName, paramCount);
if (method != null) {
return method;
}
// Check parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
return ((ScriptTypeInfo) superClass).getMethodWithParamCountInHierarchy(methodName, paramCount);
} else {
// For Java classes, reflection already handles inheritance
// We need to manually search for matching method
List<MethodInfo> overloads = superClass.getAllMethodOverloads(methodName);
for (MethodInfo m : overloads) {
if (m.getParameterCount() == paramCount) {
return m;
}
}
}
}
return null;
}
/**
* Get all method overloads with the given name from this type or any of its parent classes.
* This recursively searches up the inheritance tree and returns all matching overloads.
*/
public List<MethodInfo> getAllMethodOverloadsInHierarchy(String methodName) {
List<MethodInfo> result = new ArrayList<>();
// Get overloads from this class
result.addAll(getAllMethodOverloads(methodName));
// Get overloads from parent class recursively
if (superClass != null && superClass.isResolved()) {
if (superClass instanceof ScriptTypeInfo) {
result.addAll(((ScriptTypeInfo) superClass).getAllMethodOverloadsInHierarchy(methodName));
} else {
// For Java classes, getAllMethodOverloads already checks inheritance via reflection
result.addAll(superClass.getAllMethodOverloads(methodName));
}
}
return result;
}
/**
* Override getBestMethodOverload to search the inheritance hierarchy.
* This ensures that method overload resolution considers parent class methods.
*/
@Override
public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) {
// Get all overloads from this class and parent classes
List<MethodInfo> allOverloads = getAllMethodOverloadsInHierarchy(methodName);
if (allOverloads.isEmpty())
return null;
// If no arguments provided, try to find zero-arg method
if (argTypes == null || argTypes.length == 0) {
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == 0) {
return method;
}
}
// Fall back to first overload if no zero-arg found
return allOverloads.get(0);
}
// Phase 1: Try exact match
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == argTypes.length) {
boolean exactMatch = true;
List<FieldInfo> params = method.getParameters();
for (int i = 0; i < argTypes.length; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
TypeInfo argType = argTypes[i];
if (paramType == null || argType == null || !paramType.equals(argType)) {
exactMatch = false;
break;
}
}
if (exactMatch)
return method;
}
}
// Phase 2: Try compatible type match (assignability)
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == argTypes.length) {
boolean compatible = true;
List<FieldInfo> params = method.getParameters();
for (int i = 0; i < argTypes.length; i++) {
TypeInfo paramType = params.get(i).getTypeInfo();
TypeInfo argType = argTypes[i];
if (paramType == null || argType == null) {
continue; // Allow null types (unresolved)
}
// Check if argType can be assigned to paramType
if (!noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible(paramType,
argType)) {
compatible = false;
break;
}
}
if (compatible)
return method;
}
}
// Phase 3: Fall back to first overload with matching parameter count
for (MethodInfo method : allOverloads) {
if (method.getParameterCount() == argTypes.length) {
return method;
}
}
// No match found, return first overload
return allOverloads.get(0);
}
/**
* Override getBestMethodOverload with return type expectation to search the inheritance hierarchy.
*/
@Override
public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedReturnType) {
List<MethodInfo> allOverloads = getAllMethodOverloadsInHierarchy(methodName);
if (allOverloads.isEmpty())
return null;
// If no expected return type, return first overload
if (expectedReturnType == null) {
return allOverloads.get(0);
}
// First pass: look for return type compatible overload
for (MethodInfo method : allOverloads) {
TypeInfo returnType = method.getReturnType();
if (returnType != null && noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker.isTypeCompatible(
expectedReturnType, returnType)) {
return method;
}
}
// Second pass: return any overload (first one)
return allOverloads.get(0);
}
// ==================== INNER CLASS LOOKUP ====================
/**
* Get the dot-separated display name for this type.
* Top-level: "MyClass"
* Inner: "Outer.Inner"
* Deeply nested: "Outer.Middle.Inner"
*
* This is what users write in type annotations and what resolveType() should accept.
* Recursively walks the outerClass chain to build the full user-facing qualified name.
*/
public String getDotSeparatedName() {
if (outerClass == null) {
return getSimpleName();
}
// Recurse up the nesting hierarchy: each level prepends "Parent." to the name
return outerClass.getDotSeparatedName() + "." + getSimpleName();
}
/**
* Find an inner class by name.
*/
public ScriptTypeInfo getInnerClass(String name) {
for (ScriptTypeInfo inner : innerClasses) {
if (inner.getSimpleName().equals(name)) {
return inner;
}
}
return null;
}
// 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() + "}";
}
// ==================== 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.
*/
// Error tracking
private ErrorType errorType = ErrorType.NONE;
private String errorMessage;
private final List<MissingMethodError> missingMethodErrors = new ArrayList<>();
private final List<ConstructorMismatchError> 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<MissingMethodError> getMissingMethodErrors() { return new ArrayList<>(missingMethodErrors); }
public List<ConstructorMismatchError> 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 signature) {
missingMethodErrors.add(new MissingMethodError(interfaceType, 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();
}
public static class MissingMethodError {
private final TypeInfo interfaceType;
private final String signature;
public MissingMethodError(TypeInfo interfaceType, String signature) {
this.interfaceType = interfaceType;
this.signature = signature;
}
public String getMessage() {
return "Class must implement method '" + signature + "' from interface " + interfaceType.getSimpleName();
}
}
/**
* Represents a constructor mismatch error.
*/
public static class ConstructorMismatchError {
private final TypeInfo parentType;
private final String parentConstructorSignature;
public ConstructorMismatchError(TypeInfo parentType, String parentConstructorSignature) {
this.parentType = parentType;
this.parentConstructorSignature = parentConstructorSignature;
}
public String getMessage() {
return "Class extends " + parentType.getSimpleName() + " but has no constructor matching " + parentConstructorSignature;
}
}
// ==================== VALIDATION ====================
/**
* Validate this script type for missing interface methods and constructor matching.
*/
@Override
public void validate() {
// Check that extending class has a matching constructor
if (hasSuperClass()) {
TypeInfo superClass = getSuperClass();
if (superClass == null || !superClass.isResolved()) {
setError(ErrorType.UNRESOLVED_PARENT,
"Cannot resolve parent class " + getSuperClassName());
} else {
validateConstructorChain(superClass);
}
}
// Skip validation for interfaces - they don't implement methods
if (getKind() == Kind.INTERFACE)
return;
// Check that all interface methods are implemented
if (hasImplementedInterfaces()) {
for (TypeInfo iface : getImplementedInterfaces()) {
if (iface == null || !iface.isResolved()) {
// Mark unresolved interface error
setError(ErrorType.UNRESOLVED_INTERFACE, "Cannot resolve interface");
continue;
}
validateInterfaceImplementation(iface);
}
}
}
/**
* Validate that this script type implements all methods from an interface.
*/
private void validateInterfaceImplementation(TypeInfo iface) {
// Handle Java interfaces
Class<?> javaClass = iface.getJavaClass();
if (javaClass != null && javaClass.isInterface()) {
try {
for (Method javaMethod : javaClass.getMethods()) {
// Skip static and default methods
if (Modifier.isStatic(javaMethod.getModifiers()))
continue;
// Check if this type has a matching method
String methodName = javaMethod.getName();
int paramCount = javaMethod.getParameterCount();
boolean found = false;
List<MethodInfo> overloads = getAllMethodOverloads(methodName);
for (MethodInfo method : overloads) {
if (parameterTypesMatch(method, javaMethod)) {
found = true;
break;
}
}
//Limit to one error at a time to not spam all missing methods
if (!found && missingMethodErrors.isEmpty())
addMissingMethodError(iface, MethodSignature.asString(javaMethod));
}
} catch (Exception e) {
}
return;
}
// Handle script-defined interfaces (ScriptType)
if (iface instanceof ScriptTypeInfo) {
ScriptTypeInfo ifaceType = (ScriptTypeInfo) iface;
// Check all methods declared in the interface
for (MethodInfo ifaceMethod : ifaceType.getAllMethodsFlat()) {
MethodSignature ifaceSignature = ifaceMethod.getSignature();
String methodName = ifaceMethod.getName();
boolean found = false;
List<MethodInfo> overloads = getAllMethodOverloads(methodName);
for (MethodInfo method : overloads) {
if (method.getSignature().equals(ifaceSignature)) {
found = true;
break;
}
}
//Limit to one error at a time to not spam all missing methods
if (!found && missingMethodErrors.isEmpty())
addMissingMethodError(iface, ifaceSignature.toString());
}
}
}
/**
* Validate that this script type has a constructor compatible with its parent class.
* This checks that for each parent constructor, there's a matching constructor in the child.
*/
private void validateConstructorChain(TypeInfo superClass) {
// If no constructors defined in script type, it has an implicit default constructor
// Check if parent has a no-arg constructor
if (!hasConstructors()) {
boolean parentHasNoArg = false;
if (superClass instanceof ScriptTypeInfo) {
ScriptTypeInfo parentScript = (ScriptTypeInfo) superClass;
if (!parentScript.hasConstructors() || parentScript.findConstructor(0) != null) {
parentHasNoArg = true;
}
} else {
Class<?> javaClass = superClass.getJavaClass();
if (javaClass != null) {
try {
for (java.lang.reflect.Constructor<?> ctor : javaClass.getConstructors()) {
if (ctor.getParameterCount() == 0) {
parentHasNoArg = true;
break;
}
}
} catch (Exception e) {
// Security error
}
}
}
if (!parentHasNoArg) {
addConstructorMismatchError(superClass, superClass.getSimpleName());
}
}
// If script type has constructors, we'd need to check super() calls - that's more complex
// For now, we just validate the implicit default constructor case
}
/**
* Check if a MethodInfo's parameter types match a Java reflection Method's parameter types.
*/
private boolean parameterTypesMatch(MethodInfo methodInfo, Method javaMethod) {
List<FieldInfo> 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).getTypeInfo();
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;
}
}
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more