mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-08 18:54:50 +00:00
Merge branch 'dev'
This commit is contained in:
commit
985e03c227
71 files changed
+3125
-989
No files matched your search
@@ -0,0 +1,98 @@
|
||||
# CustomNPC+ 1.11 Changelog
|
||||
|
||||
## Ability System
|
||||
- Phase-based abilities (Wind-up, Active, Dazed) with configurable cooldowns, targeting, range, interrupts, invulnerability, and magic damage
|
||||
- **Melee:** Slam, Heavy Hit, Cutter
|
||||
- **Energy Projectiles:** Orb, Disc, Beam, Laser Shot with variant system, homing, hit types (Single/Pierce/Multi), size interpolation, explosions
|
||||
- **Barriers:** Dome, Wall, Shield with HP, reflection, damage absorption
|
||||
- **Defensive:** Guard, Counter, Dodge
|
||||
- **Movement:** Charge, Dash, Teleport
|
||||
- **AOE:** Shockwave, Sweeper, Vortex
|
||||
- **Hazards & Traps:** Persistent zones and proximity triggers with visual presets
|
||||
- **Effect:** Healing, buffs, debuffs with target filtering (Allies/Enemies/All)
|
||||
- **Custom Ability:** Blank-slate type with full script hooks and toggle modes
|
||||
- Burst system, chained ability combos with concurrency, telegraph system (6 shapes)
|
||||
- Per-phase animations and sound events
|
||||
- Ability conditions: HP Threshold, Hit Count, Has Item, Quest Completed, Has Effect
|
||||
- 12-slot player ability hotbar with HUD editor
|
||||
- Full scripting API for ability and energy projectile lifecycle events
|
||||
- Ability Extender system for custom hooks
|
||||
- Live preview in editor
|
||||
- Commands: `/kam ability list|types|prebuilts|info|delete|reload|give|remove|giveChain|removeChain|player`
|
||||
|
||||
## Auction House
|
||||
- Global marketplace with bidding, buyout, and snipe protection
|
||||
- Built-in currency system (configurable, profile-persistent, Vault support)
|
||||
- Listing fees, sales tax, minimum prices
|
||||
- My Trades panel with claim management (20-day expiry)
|
||||
- Search/sort, notifications (online + offline summary)
|
||||
- Trade slots (default 8, expandable via permissions)
|
||||
- Item blacklist (by item, wildcard, mod, NBT; soulbound always blocked)
|
||||
- Auctioneer NPC role
|
||||
- Admin GUI and logging
|
||||
- Cancelable scripting events: Create, Bid, Buyout, Cancel, Claim
|
||||
|
||||
## Script Editor
|
||||
- Full code editor with syntax highlighting, autocomplete, error detection, and hover tooltips
|
||||
- Go to Line, Search/Replace, Go to Definition, Fullscreen
|
||||
- Undo/Redo, Duplicate Line, Comment Toggle, Rename, Move Lines
|
||||
- Line numbers, smooth scrolling, indent guides, brace matching, smart brackets
|
||||
|
||||
## Java Scripting
|
||||
- Write scripts in Java via Janino compiler with automatic hook resolution
|
||||
- Per-tab language selector (mix Java and JavaScript)
|
||||
- External `.java` file support
|
||||
|
||||
## Client-Side Scripting
|
||||
- Server-controlled client script execution with auto-sync (off by default)
|
||||
|
||||
## Addon API Support
|
||||
- Ship `.d.ts` type definitions for automatic script editor autocomplete
|
||||
- Patch support for extending CNPC+ types, hot reload
|
||||
|
||||
## Traders
|
||||
- Stock system: Global or Per-Player modes with configurable restock timers
|
||||
- Currency trades (items + coins)
|
||||
- Client-side balance preview
|
||||
|
||||
## Cloner
|
||||
- Folder and subfolder organization for all categories
|
||||
- Full-width directory browser, improved tabs, tag-based filtering
|
||||
|
||||
## Animations
|
||||
- Data store for passing data across events/frames
|
||||
- Consumer-based task system
|
||||
- Expanded scripting API
|
||||
|
||||
## Animated Textures
|
||||
- Frame-based animation for items, effect icons, ability hotbar, scripted GUIs
|
||||
|
||||
## NPC API
|
||||
- `IHitboxData` (width/height scaling), `ITintData` (hurt tint, persistent tints)
|
||||
- Scriptable behavior: sprint, swim, doors, fire reactions, tactical chance, walking range
|
||||
- `ISound` (volume, pitch, repeat, position)
|
||||
- Skin overlay `getColor()`/`setColor()`, knockback API
|
||||
- Six overlays in singleplayer, script error reporting to console
|
||||
|
||||
## NPC Combat & Behavior
|
||||
- Attack speed enforcement, passive faction protection
|
||||
- Improved track speed, invalid texture fallback
|
||||
|
||||
## Other Changes
|
||||
- Player Dialog Events match NPC Dialog Events
|
||||
- Linked Item attack speed and scripting API
|
||||
- Right-click cycling on multi-option buttons
|
||||
- Legacy quest import (1.6.4), fullscreen/pannable GUIs, unified menu background
|
||||
- Textfield CTRL shortcuts, `java.lang.reflect` banned in scripts
|
||||
- Entity packet loss fix (invisible NPCs)
|
||||
- Natural spawn GUI, runtime control, and despawn improvements
|
||||
|
||||
## Bug Fixes
|
||||
- Script config sync, Bukkit integration
|
||||
- Quest cooldown for MC/RL Custom timers, quest/dialog saving and categories
|
||||
- Bard music restart on dialog open
|
||||
- Spawner yaw, biome spawn persistence, NPC invisibility
|
||||
- Track speed, set speed edge cases, spawn data, block waypoints, job spawner
|
||||
- Hitbox scaling and collision
|
||||
- Animation offsets, lookup failures, syncing, resync on teleport, registration names, double loading
|
||||
- Mount movement jitter, dismount edge cases
|
||||
+1
-1
@@ -6,7 +6,7 @@ plugins {
|
||||
id "dts.typescript-generator"
|
||||
}
|
||||
|
||||
version = "1.11"
|
||||
version = "1.11.1"
|
||||
group= "kamkeel.customnpc-plus"
|
||||
archivesBaseName = "CustomNPC-Plus"
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import kamkeel.npcs.controllers.data.profile.Profile;
|
||||
import kamkeel.npcs.controllers.data.profile.ProfileInfoEntry;
|
||||
import kamkeel.npcs.controllers.data.profile.ProfileOperation;
|
||||
import kamkeel.npcs.controllers.data.profile.Slot;
|
||||
import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.CompressedStreamTools;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
@@ -453,6 +455,9 @@ public class ProfileController implements IProfileHandler {
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player);
|
||||
verifySlotQuests(profile.player);
|
||||
save(profile.player, profile);
|
||||
if (profile.player instanceof EntityPlayerMP) {
|
||||
ProfileSharedQuestPacket.sendToPlayer((EntityPlayerMP) profile.player);
|
||||
}
|
||||
|
||||
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player);
|
||||
EventHooks.onProfileCreate(handler, scriptPlayer, profile, newSlotId, true);
|
||||
@@ -524,6 +529,9 @@ public class ProfileController implements IProfileHandler {
|
||||
saveSlotData(profile.player, profile, prevSlot);
|
||||
loadSlotData(profile.player, profile, newSlotId);
|
||||
syncSharedQuestsToPlayer(profile.player);
|
||||
if (profile.player instanceof EntityPlayerMP) {
|
||||
ProfileSharedQuestPacket.sendToPlayer((EntityPlayerMP) profile.player);
|
||||
}
|
||||
|
||||
EventHooks.onProfileChange(handler, scriptPlayer, profile, newSlotId, prevSlot, true);
|
||||
} else {
|
||||
@@ -901,5 +909,8 @@ public class ProfileController implements IProfileHandler {
|
||||
profile.sharedQuestTimestamps.put(questId, completeTime);
|
||||
}
|
||||
save(player, profile);
|
||||
if (player instanceof EntityPlayerMP) {
|
||||
ProfileSharedQuestPacket.sendToPlayer((EntityPlayerMP) player);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.enums.EnumSyncAction;
|
||||
import kamkeel.npcs.network.enums.EnumSyncType;
|
||||
import kamkeel.npcs.network.packets.data.LoginPacket;
|
||||
import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket;
|
||||
import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket;
|
||||
import kamkeel.npcs.network.packets.data.ability.AbilityHotbarSyncPacket;
|
||||
import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket;
|
||||
@@ -184,6 +185,7 @@ public class SyncController {
|
||||
DBCAddon.instance.syncPlayer(player);
|
||||
syncPlayerData(player, false);
|
||||
PartyInfoPacket.sendPartyData(player);
|
||||
ProfileSharedQuestPacket.sendToPlayer(player);
|
||||
|
||||
// Sync skin overlays after full handshake to ensure the client is ready.
|
||||
// Overlays are not part of getSyncNBTFull(), and packets sent during
|
||||
|
||||
@@ -586,6 +586,7 @@ public class PacketHandler {
|
||||
DATA_PACKET.registerPacket(new UpdateAnimationsPacket());
|
||||
DATA_PACKET.registerPacket(new VillagerListPacket());
|
||||
DATA_PACKET.registerPacket(new PlayerDataInfoPacket());
|
||||
DATA_PACKET.registerPacket(new ProfileSharedQuestPacket());
|
||||
|
||||
// Data | GUI Packets
|
||||
DATA_PACKET.registerPacket(new GuiClosePacket());
|
||||
|
||||
@@ -67,6 +67,9 @@ public enum EnumDataPacket {
|
||||
ABILITY_HOTBAR_SYNC,
|
||||
ABILITY_COOLDOWN_SYNC,
|
||||
|
||||
// Profile System
|
||||
PROFILE_SHARED_QUEST,
|
||||
|
||||
// Auction System
|
||||
AUCTION_DATA,
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package kamkeel.npcs.network.packets.data;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.enums.EnumDataPacket;
|
||||
import kamkeel.npcs.util.ByteBufUtils;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.ProfileClientConfig;
|
||||
import noppes.npcs.controllers.ProfileConfigSync;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class ProfileSharedQuestPacket extends AbstractPacket {
|
||||
public static final String packetName = "Data|ProfileSharedQuest";
|
||||
|
||||
private NBTTagCompound compound;
|
||||
|
||||
public ProfileSharedQuestPacket() {
|
||||
}
|
||||
|
||||
public ProfileSharedQuestPacket(NBTTagCompound compound) {
|
||||
this.compound = compound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumDataPacket.PROFILE_SHARED_QUEST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.DATA_PACKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
ByteBufUtils.writeNBT(out, this.compound);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
NBTTagCompound nbt = ByteBufUtils.readNBT(in);
|
||||
ProfileClientConfig.readFromNBT(nbt);
|
||||
}
|
||||
|
||||
public static void sendToPlayer(EntityPlayerMP player) {
|
||||
NBTTagCompound compound = ProfileConfigSync.writeToNBT(player);
|
||||
PacketHandler.Instance.sendToPlayer(new ProfileSharedQuestPacket(compound), player);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11")
|
||||
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11.1")
|
||||
public class CustomNpcs {
|
||||
|
||||
@SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy")
|
||||
|
||||
@@ -92,6 +92,7 @@ import noppes.npcs.scripted.item.ScriptItemStack;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class EventHooks {
|
||||
public EventHooks() {
|
||||
@@ -1053,12 +1054,24 @@ public class EventHooks {
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
private static final HashMap<Class<?>, String> forgeEventNameCache = new HashMap<>();
|
||||
|
||||
private static String getForgeEventName(Event event) {
|
||||
Class<?> clazz = event.getClass();
|
||||
String cached = forgeEventNameCache.get(clazz);
|
||||
if (cached != null)
|
||||
return cached;
|
||||
String eventName = clazz.getName();
|
||||
int i = eventName.lastIndexOf(".");
|
||||
eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", ""));
|
||||
forgeEventNameCache.put(clazz, eventName);
|
||||
return eventName;
|
||||
}
|
||||
|
||||
public static void onForgeEvent(ForgeEvent ev, Event event) {
|
||||
ForgeDataScript handler = ScriptController.Instance.forgeScripts;
|
||||
if (handler.isEnabled()) {
|
||||
String eventName = event.getClass().getName();
|
||||
int i = eventName.lastIndexOf(".");
|
||||
eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", ""));
|
||||
String eventName = getForgeEventName(event);
|
||||
if (event.isCancelable()) {
|
||||
ev.setCanceled(event.isCanceled());
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ public class ClientCacheHandler {
|
||||
ClientCacheHandler.customOverlays.clear();
|
||||
ClientCacheHandler.skinOverlays.clear();
|
||||
ClientCacheHandler.playerAnimations.clear();
|
||||
ProfileClientConfig.reset();
|
||||
|
||||
// Clear Texture Caches
|
||||
GuiSoundSelection.cachedDomains.clear();
|
||||
|
||||
@@ -930,6 +930,28 @@ public class ClientProxy extends CommonProxy {
|
||||
return Minecraft.getMinecraft().fontRenderer.getStringWidth(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of a string when rendered with the specified font style (bold/italic).
|
||||
*
|
||||
* @param text the string to measure
|
||||
* @param fontStyle combination of {@link java.awt.Font#BOLD} and {@link java.awt.Font#ITALIC},
|
||||
* or {@link java.awt.Font#PLAIN} for normal style
|
||||
* @return the width in pixels
|
||||
*/
|
||||
public int width(String text, int fontStyle) {
|
||||
if (useCustomFont)
|
||||
return textFont.getStringWidth(text, fontStyle);
|
||||
if (fontStyle == java.awt.Font.PLAIN)
|
||||
return Minecraft.getMinecraft().fontRenderer.getStringWidth(text);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if ((fontStyle & java.awt.Font.BOLD) != 0)
|
||||
sb.append('\u00A7').append('l');
|
||||
if ((fontStyle & java.awt.Font.ITALIC) != 0)
|
||||
sb.append('\u00A7').append('o');
|
||||
sb.append(text);
|
||||
return Minecraft.getMinecraft().fontRenderer.getStringWidth(sb.toString());
|
||||
}
|
||||
|
||||
public FontContainer copy() {
|
||||
FontContainer font = new FontContainer();
|
||||
font.textFont = textFont;
|
||||
@@ -939,7 +961,7 @@ public class ClientProxy extends CommonProxy {
|
||||
|
||||
public void drawString(String text, int x, int y, int color) {
|
||||
if (useCustomFont) {
|
||||
textFont.renderString(text, x, y, color, true);
|
||||
// textFont.renderString(text, x, y, color, true);
|
||||
textFont.renderString(text, x, y, color, false);
|
||||
} else {
|
||||
Minecraft.getMinecraft().fontRenderer.drawStringWithShadow(text, x, y, color);
|
||||
|
||||
@@ -10,12 +10,12 @@ 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 noppes.npcs.client.gui.util.script.interpreter.token.ScriptColorScheme;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.*;
|
||||
|
||||
public class CustomNpcResourceListener implements
|
||||
IResourceManagerReloadListener {
|
||||
@@ -30,8 +30,11 @@ public class CustomNpcResourceListener implements
|
||||
SimpleReloadableResourceManager simplemanager = (SimpleReloadableResourceManager) var1;
|
||||
|
||||
FolderResourcePack pack = new FolderResourcePack(CustomNpcs.Dir);
|
||||
LogWriter.info("[ResourceListener] Adding FolderResourcePack (as fallback): " + CustomNpcs.Dir.getAbsolutePath());
|
||||
simplemanager.reloadResourcePack(pack);
|
||||
|
||||
ScriptColorScheme.reloadColorScheme(var1);
|
||||
|
||||
// Reload scripting .d.ts files
|
||||
JSTypeRegistry registry = JSTypeRegistry.getInstance();
|
||||
registry.clear();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package noppes.npcs.client;
|
||||
|
||||
import kamkeel.npcs.controllers.ProfileController;
|
||||
import kamkeel.npcs.controllers.data.profile.Profile;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.config.ConfigMain;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ProfileClientConfig {
|
||||
private static volatile Map<Integer, Long> sharedQuestTimestamps = new HashMap<>();
|
||||
|
||||
public static void readFromNBT(NBTTagCompound compound) {
|
||||
if (compound == null) return;
|
||||
|
||||
Map<Integer, Long> timestamps = new HashMap<>();
|
||||
if (compound.hasKey("SharedQuestTimestamps")) {
|
||||
NBTTagList list = compound.getTagList("SharedQuestTimestamps", 10);
|
||||
for (int i = 0; i < list.tagCount(); i++) {
|
||||
NBTTagCompound entry = list.getCompoundTagAt(i);
|
||||
timestamps.put(entry.getInteger("Quest"), entry.getLong("Date"));
|
||||
}
|
||||
}
|
||||
sharedQuestTimestamps = timestamps;
|
||||
}
|
||||
|
||||
public static void reset() {
|
||||
sharedQuestTimestamps = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether profiles are enabled, checking the correct source for each side.
|
||||
*/
|
||||
public static boolean isProfilesEnabled() {
|
||||
if (ProfileController.Instance != null)
|
||||
return ConfigMain.ProfilesEnabled;
|
||||
return ClientCacheHandler.allowProfiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a quest has a shared timestamp, resolving from server or client cache.
|
||||
*/
|
||||
public static boolean hasSharedQuest(EntityPlayer player, int questId) {
|
||||
if (ProfileController.Instance != null) {
|
||||
Profile profile = ProfileController.Instance.getProfile(player);
|
||||
return profile != null && profile.sharedQuestTimestamps.containsKey(questId);
|
||||
}
|
||||
return sharedQuestTimestamps.containsKey(questId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared quest timestamp, resolving from server or client cache.
|
||||
*/
|
||||
public static Long getSharedQuestTimestamp(EntityPlayer player, int questId) {
|
||||
if (ProfileController.Instance != null) {
|
||||
Profile profile = ProfileController.Instance.getProfile(player);
|
||||
return profile != null ? profile.sharedQuestTimestamps.get(questId) : null;
|
||||
}
|
||||
return sharedQuestTimestamps.get(questId);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,8 @@ public class GuiSoundSelection extends SubGuiInterface implements ICustomScrollL
|
||||
scrollSounds.resetScroll();
|
||||
getTextField(66).setText("");
|
||||
soundSearch = "";
|
||||
initGui();
|
||||
scrollSounds.setList(domains.get(selectedDomain));
|
||||
getButton(1).enabled = false;
|
||||
}
|
||||
if (guiCustomScroll.id == 1) {
|
||||
selectedResource = new ResourceLocation(selectedDomain, guiCustomScroll.getSelected());
|
||||
|
||||
@@ -19,6 +19,7 @@ import noppes.npcs.client.gui.util.script.interpreter.field.FieldAccessInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodCallInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.method.MethodInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.Token;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.ScriptColorScheme;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.hover.GutterIconRenderer;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.hover.HoverState;
|
||||
@@ -556,9 +557,13 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
int cursorCol = 0;
|
||||
if (container != null && container.lines != null && cursorLine < container.lines.size()) {
|
||||
LineData ld = container.lines.get(cursorLine);
|
||||
String lineText = ld.text;
|
||||
int cursorOffset = selection.getCursorPosition() - ld.start;
|
||||
cursorCol = ClientProxy.Font.width(lineText.substring(0, Math.min(cursorOffset, lineText.length())));
|
||||
ScriptLine sl = container.getDocument() != null ? container.getDocument().getLine(cursorLine) : null;
|
||||
if (sl != null) {
|
||||
cursorCol = sl.getRenderedWidth(0, Math.min(cursorOffset, ld.text.length()));
|
||||
} else {
|
||||
cursorCol = ClientProxy.Font.width(ld.text.substring(0, Math.min(cursorOffset, ld.text.length())));
|
||||
}
|
||||
}
|
||||
|
||||
int screenX = GuiScriptTextArea.this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + cursorCol;
|
||||
@@ -597,18 +602,18 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
// Draw outer border around entire area
|
||||
int offset = fullscreen() ? 2 : 1;
|
||||
drawRect(x - offset, y - offset - searchBar.getTotalHeight(), x + width + offset, y + height + offset,
|
||||
0xffa0a0a0);
|
||||
ScriptColorScheme.getBackgroundStyle().getBorderColor());
|
||||
|
||||
int searchHeight = searchBar.getTotalHeight();
|
||||
|
||||
|
||||
// Draw line number gutter background
|
||||
int viewportX = x + LINE_NUMBER_GUTTER_WIDTH;
|
||||
drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000);
|
||||
drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, ScriptColorScheme.getBackgroundStyle().getGutterColor());
|
||||
// Draw text viewport background (starts after gutter)
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000);
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, ScriptColorScheme.getBackgroundStyle().getBackgroundColor());
|
||||
// Draw separator line between gutter and text area
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH-1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41);
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH-1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, ScriptColorScheme.getBackgroundStyle().getGutterSeparatorColor());
|
||||
|
||||
// Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter)
|
||||
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
@@ -624,9 +629,15 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
((GuiNPCInterface) listener).mouseScroll = wheelDelta;
|
||||
|
||||
// Intercept wheel scroll for tooltip (highest priority)
|
||||
if (wheelDelta != 0 && hoverState.isTooltipVisible() && hoverState.isMouseOverTooltip(xMouse, yMouse)) {
|
||||
hoverState.scrollTooltip(wheelDelta);
|
||||
wheelDelta = 0;
|
||||
// Only consume the wheel event when the tooltip actually has content to scroll;
|
||||
// otherwise let it fall through to the editor so non-scrollable tooltips don't eat input.
|
||||
if (wheelDelta != 0 && hoverState.isTooltipVisible()) {
|
||||
if (hoverState.isMouseOverTooltip(xMouse, yMouse)) {
|
||||
hoverState.scrollTooltip(wheelDelta);
|
||||
wheelDelta = 0;
|
||||
} else {
|
||||
hoverState.clearHover();
|
||||
}
|
||||
}
|
||||
|
||||
// Let autocomplete menu consume scroll first if visible
|
||||
@@ -833,13 +844,13 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5 - GutterIconRenderer.ICON_GUTTER_WIDTH; // right-align before icon space
|
||||
int lineNumY = posY + 1;
|
||||
// Highlight current line number
|
||||
int lineNumColor = 0xFF606366;
|
||||
int lineNumColor = ScriptColorScheme.getBackgroundStyle().getLineNumberColor();
|
||||
if (active && isEnabled()) {
|
||||
for (int li = 0; li < list.size(); li++) {
|
||||
LineData ld = list.get(li);
|
||||
if (selection.getCursorPosition() >= ld.start && selection.getCursorPosition() < ld.end || (li == list.size() - 1 && selection.getCursorPosition() == text.length())) {
|
||||
if (li == i) {
|
||||
lineNumColor = 0xFFb9c7d6;
|
||||
lineNumColor = ScriptColorScheme.getBackgroundStyle().getLineNumberActiveColor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -860,13 +871,13 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
//Highlight braces the cursor position is on
|
||||
if (startBracket != endBracket) {
|
||||
if (startBracket >= data.start && startBracket < data.end) {
|
||||
int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start));
|
||||
int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1;
|
||||
int s = scriptLine.getRenderedWidth(0, startBracket - data.start);
|
||||
int e = scriptLine.getRenderedWidth(0, startBracket - data.start + 1) + 1;
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00);
|
||||
}
|
||||
if (endBracket >= data.start && endBracket < data.end) {
|
||||
int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start));
|
||||
int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1;
|
||||
int s = scriptLine.getRenderedWidth(0, endBracket - data.start);
|
||||
int e = scriptLine.getRenderedWidth(0, endBracket - data.start + 1) + 1;
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00);
|
||||
}
|
||||
}
|
||||
@@ -876,8 +887,8 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
for (int ubPos : unmatchedBraces) {
|
||||
if (ubPos >= data.start && ubPos < data.end) {
|
||||
int rel = ubPos - data.start;
|
||||
int s = ClientProxy.Font.width(line.substring(0, rel));
|
||||
int e = ClientProxy.Font.width(line.substring(0, rel + 1)) + 1;
|
||||
int s = scriptLine.getRenderedWidth(0, rel);
|
||||
int e = scriptLine.getRenderedWidth(0, rel + 1) + 1;
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0xffcc0000);
|
||||
}
|
||||
}
|
||||
@@ -887,8 +898,8 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
Matcher m = container.regexWord.matcher(line);
|
||||
while (m.find()) {
|
||||
if (line.substring(m.start(), m.end()).equals(highlightedWord)) {
|
||||
int s = ClientProxy.Font.width(line.substring(0, m.start()));
|
||||
int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1;
|
||||
int s = scriptLine.getRenderedWidth(0, m.start());
|
||||
int e = scriptLine.getRenderedWidth(0, m.end()) + 1;
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x99004c00);
|
||||
}
|
||||
}
|
||||
@@ -905,8 +916,8 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
int matchStart = Math.max(match[0] - data.start, 0);
|
||||
int matchEnd = Math.min(match[1] - data.start, line.length());
|
||||
if (matchStart < matchEnd) {
|
||||
int s = ClientProxy.Font.width(line.substring(0, matchStart));
|
||||
int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1;
|
||||
int s = scriptLine.getRenderedWidth(0, matchStart);
|
||||
int e = scriptLine.getRenderedWidth(0, matchEnd) + 1;
|
||||
boolean isExcluded = searchBar.isMatchExcluded(mi);
|
||||
// Current match gets brighter highlight, others get dimmer
|
||||
int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa;
|
||||
@@ -940,9 +951,9 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
boolean isEmpty = (occ[0] == occ[1]);
|
||||
|
||||
if (occStart <= occEnd) { // Changed from < to <= to handle empty case
|
||||
int s = ClientProxy.Font.width(line.substring(0, occStart));
|
||||
int e = isEmpty ? s + 2 : ClientProxy.Font.width(
|
||||
line.substring(0, occEnd)) + 1; // 2px wide for empty
|
||||
int s = scriptLine.getRenderedWidth(0, occStart);
|
||||
int e = isEmpty ? s + 2 : scriptLine.getRenderedWidth(
|
||||
0, occEnd) + 1; // 2px wide for empty
|
||||
int occX = x + LINE_NUMBER_GUTTER_WIDTH + s;
|
||||
int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 2 + e;
|
||||
boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]);
|
||||
@@ -974,7 +985,7 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
if (currentWord != null && cursorInWord >= 0 && cursorInWord <= currentWord.length()) {
|
||||
String beforeCursor = currentWord.substring(0,
|
||||
Math.min(cursorInWord, currentWord.length()));
|
||||
int cursorX = occX + ClientProxy.Font.width(beforeCursor);
|
||||
int cursorX = occX + scriptLine.getRenderedWidth(occStart, occStart + beforeCursor.length());
|
||||
// drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1,
|
||||
// 0xFFFFFFFF);
|
||||
}
|
||||
@@ -992,10 +1003,10 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
// Highlight selection
|
||||
if (selection.hasSelection() && selection.getEndSelection() > data.start && selection.getStartSelection() <= data.end) {
|
||||
if (selection.getStartSelection() < data.end) {
|
||||
int s = ClientProxy.Font.width(
|
||||
line.substring(0, Math.max(selection.getStartSelection() - data.start, 0)));
|
||||
int e = ClientProxy.Font.width(
|
||||
line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1;
|
||||
int s = scriptLine.getRenderedWidth(
|
||||
0, Math.max(selection.getStartSelection() - data.start, 0));
|
||||
int e = scriptLine.getRenderedWidth(
|
||||
0, Math.min(selection.getEndSelection() - data.start, w)) + 1;
|
||||
drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff);
|
||||
}
|
||||
}
|
||||
@@ -1050,9 +1061,9 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
// Draw cursor: pause blinking while user is active recently
|
||||
boolean recentInput = selection.hadRecentInput();
|
||||
if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) {
|
||||
int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width(
|
||||
line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length())));
|
||||
drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff);
|
||||
int posX = x + LINE_NUMBER_GUTTER_WIDTH + scriptLine.getRenderedWidth(
|
||||
0, Math.min(selection.getCursorPosition() - data.start, line.length()));
|
||||
drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, ScriptColorScheme.getBackgroundStyle().getCaretColor());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1078,7 +1089,7 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
double linesCount = (double) effLines;
|
||||
int posY = (int) (y + 1f * scroll.getScrollPos() / linesCount * (height - 4)) + 1;
|
||||
|
||||
drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0);
|
||||
drawRect(posX, posY, posX + 5, posY + sbSize + 2, ScriptColorScheme.getBackgroundStyle().getScrollbarColor());
|
||||
}
|
||||
|
||||
if (parent != null)
|
||||
@@ -1143,11 +1154,14 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines +1) {
|
||||
double yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight;
|
||||
if (yMouseD >= yPos && yMouseD < yPos + this.container.lineHeight) {
|
||||
ScriptLine scriptLine = container.getDocument() != null ? container.getDocument().getLine(i) : null;
|
||||
int lineWidth = 0;
|
||||
char[] chars = data.text.toCharArray();
|
||||
|
||||
for (int j = 1; j <= chars.length; ++j) {
|
||||
int w = ClientProxy.Font.width(data.text.substring(0, j));
|
||||
int w = scriptLine != null
|
||||
? scriptLine.getRenderedWidth(0, j)
|
||||
: ClientProxy.Font.width(data.text.substring(0, j));
|
||||
if (xMouse < lineWidth + (w - lineWidth) / 2) {
|
||||
return data.start + j - 1;
|
||||
}
|
||||
@@ -1225,9 +1239,9 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, lineText.length()));
|
||||
tokenLocalEnd = Math.max(0, Math.min(tokenLocalEnd, lineText.length()));
|
||||
|
||||
int tokenScreenX = viewportX + ClientProxy.Font.width(lineText.substring(0, tokenLocalStart));
|
||||
int tokenScreenX = viewportX + lineData.getRenderedWidth(0, tokenLocalStart);
|
||||
int tokenScreenY = y + (lineIdx - scroll.getScrolledLine()) * container.lineHeight - (int)fracPixels;
|
||||
int tokenWidth = ClientProxy.Font.width(lineText.substring(tokenLocalStart, tokenLocalEnd));
|
||||
int tokenWidth = lineData.getRenderedWidth(tokenLocalStart, tokenLocalEnd);
|
||||
|
||||
return new Object[] { token, tokenScreenX, tokenScreenY, tokenWidth };
|
||||
}
|
||||
@@ -2570,6 +2584,34 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
String after = getSelectionAfterText();
|
||||
int cursorPos = selection.getCursorPosition();
|
||||
|
||||
// Diamond/generic angle bracket handling
|
||||
if (c == '<') {
|
||||
if (!container.getDocument().isExcludedInclusive(cursorPos) && isGenericContext(before)) {
|
||||
if (hasUnmatchedCloserAhead(after, '<', '>')) {
|
||||
// There's already an unmatched '>' ahead — just insert '<'
|
||||
setText(before + "<" + after, true);
|
||||
selection.reset(before.length() + 1);
|
||||
} else {
|
||||
setText(before + "<>" + after, true);
|
||||
selection.reset(before.length() + 1);
|
||||
}
|
||||
scrollToCursor();
|
||||
autocompleteManager.onCharTyped(c, text, cursorPos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (c == '>') {
|
||||
if (after.length() > 0 && after.charAt(0) == '>') {
|
||||
if (shouldSkipAngleBracketClose(before)) {
|
||||
selection.reset(before.length() + 1);
|
||||
scrollToCursor();
|
||||
autocompleteManager.onCharTyped(c, text, cursorPos);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((c == ')' || c == ']' || c == '}') && after.length() > 0 && after.charAt(0) == c) {
|
||||
selection.reset(before.length() + 1);
|
||||
scrollToCursor();
|
||||
@@ -2688,6 +2730,86 @@ public class GuiScriptTextArea extends GuiNpcTextField {
|
||||
return (backslashes % 2) == 1;
|
||||
}
|
||||
|
||||
private boolean isGenericContext(String before) {
|
||||
if (before == null || before.isEmpty()) return false;
|
||||
|
||||
int i = before.length() - 1;
|
||||
while (i >= 0 && (before.charAt(i) == ' ' || before.charAt(i) == '\t')) {
|
||||
i--;
|
||||
}
|
||||
if (i < 0) return false;
|
||||
|
||||
char last = before.charAt(i);
|
||||
|
||||
if (last == '>' || last == ')' || last == ']') return true;
|
||||
if (last == ',') return true;
|
||||
|
||||
if (Character.isJavaIdentifierPart(last)) {
|
||||
int end = i + 1;
|
||||
while (i >= 0 && Character.isJavaIdentifierPart(before.charAt(i))) {
|
||||
i--;
|
||||
}
|
||||
String word = before.substring(i + 1, end);
|
||||
|
||||
if (isComparisonKeyword(word)) return false;
|
||||
if (isNumericLiteral(word)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isComparisonKeyword(String word) {
|
||||
return "if".equals(word) || "while".equals(word) || "for".equals(word)
|
||||
|| "return".equals(word) || "else".equals(word) || "case".equals(word)
|
||||
|| "assert".equals(word) || "throw".equals(word) || "true".equals(word)
|
||||
|| "false".equals(word) || "null".equals(word);
|
||||
}
|
||||
|
||||
private boolean isNumericLiteral(String word) {
|
||||
if (word.isEmpty()) return false;
|
||||
char first = word.charAt(0);
|
||||
return first >= '0' && first <= '9';
|
||||
}
|
||||
|
||||
private boolean shouldSkipAngleBracketClose(String before) {
|
||||
if (before == null || before.isEmpty()) return false;
|
||||
int depth = 0;
|
||||
boolean inLineComment = false;
|
||||
boolean inBlockComment = false;
|
||||
boolean inString = false;
|
||||
boolean escape = false;
|
||||
char stringDelimiter = 0;
|
||||
|
||||
for (int i = 0; i < before.length(); i++) {
|
||||
char c = before.charAt(i);
|
||||
char next = i + 1 < before.length() ? before.charAt(i + 1) : 0;
|
||||
|
||||
if (inString) {
|
||||
if (escape) { escape = false; }
|
||||
else if (c == '\\') { escape = true; }
|
||||
else if (c == stringDelimiter || c == '\n') { inString = false; }
|
||||
continue;
|
||||
}
|
||||
if (inBlockComment) {
|
||||
if (c == '*' && next == '/') { inBlockComment = false; i++; }
|
||||
continue;
|
||||
}
|
||||
if (inLineComment) {
|
||||
if (c == '\n') { inLineComment = false; }
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && next == '/') { inLineComment = true; i++; continue; }
|
||||
if (c == '/' && next == '*') { inBlockComment = true; i++; continue; }
|
||||
if (c == '"' || c == '\'') { inString = true; stringDelimiter = c; escape = false; continue; }
|
||||
|
||||
if (c == '<') depth++;
|
||||
else if (c == '>') depth--;
|
||||
}
|
||||
return depth > 0;
|
||||
}
|
||||
|
||||
private boolean hasUnmatchedCloserAhead(String after, char opener, char closer) {
|
||||
int balance = 0;
|
||||
int len = after.length();
|
||||
|
||||
@@ -1181,13 +1181,15 @@ public class FormatHelper {
|
||||
return settings.spaceBeforeSemicolon ? " " : "";
|
||||
}
|
||||
|
||||
// ---- COLON spacing (ternary : always gets space, label : uses settings) ----
|
||||
// ---- COLON spacing (ternary : always gets space, enhanced-for : always gets space, label : uses settings) ----
|
||||
if (prev.type == TokenType.COLON) {
|
||||
if (isTernaryColon(allTokens, currIdx - 1)) return " ";
|
||||
if (isEnhancedForLoopColon(allTokens, currIdx - 1)) return " ";
|
||||
return settings.spaceAfterColon ? " " : "";
|
||||
}
|
||||
if (curr.type == TokenType.COLON) {
|
||||
if (isTernaryColon(allTokens, currIdx)) return " ";
|
||||
if (isEnhancedForLoopColon(allTokens, currIdx)) return " ";
|
||||
return settings.spaceBeforeColon ? " " : "";
|
||||
}
|
||||
|
||||
@@ -1398,6 +1400,39 @@ public class FormatHelper {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a colon at the given index is the separator in an enhanced for-loop
|
||||
* ({@code for (Type var : collection)}) by scanning backwards from the colon
|
||||
* for an opening paren preceded by the {@code for} keyword.
|
||||
*/
|
||||
private boolean isEnhancedForLoopColon(List<Token> tokens, int colonIdx) {
|
||||
int depth = 0;
|
||||
for (int i = colonIdx - 1; i >= 0; i--) {
|
||||
Token t = tokens.get(i);
|
||||
if (t.type == TokenType.PAREN_CLOSE) depth++;
|
||||
else if (t.type == TokenType.PAREN_OPEN) {
|
||||
if (depth > 0) {
|
||||
depth--;
|
||||
} else {
|
||||
// Found the opening paren at our depth — check if preceded by 'for'
|
||||
for (int j = i - 1; j >= 0; j--) {
|
||||
if (tokens.get(j).type != TokenType.WHITESPACE) {
|
||||
return tokens.get(j).type == TokenType.KEYWORD
|
||||
&& tokens.get(j).value.equals("for");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Stop at statement boundaries
|
||||
if (t.type == TokenType.SEMICOLON || t.type == TokenType.BRACE_OPEN ||
|
||||
t.type == TokenType.BRACE_CLOSE) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAssignmentOperator(String op) {
|
||||
switch (op) {
|
||||
case "=": case "+=": case "-=": case "*=": case "/=": case "%=":
|
||||
|
||||
@@ -575,7 +575,7 @@ public class JavaTextContainer extends TextContainer {
|
||||
|
||||
for (ImportEntry ie : importEntries) {
|
||||
// Highlight the 'import' keyword
|
||||
marks.add(new Mark(ie.importKwStart, ie.importKwEnd, TokenType.IMPORT_KEYWORD));
|
||||
marks.add(new Mark(ie.importKwStart, ie.importKwEnd, TokenType.KEYWORD));
|
||||
|
||||
String fullPath = ie.fullPath;
|
||||
int pathStart = ie.pathStart;
|
||||
@@ -836,7 +836,7 @@ public class JavaTextContainer extends TextContainer {
|
||||
Matcher m = CLASS_DECL.matcher(text);
|
||||
List<int[]> excluded = MethodBlock.getExcludedRanges(text);
|
||||
while (m.find()) {
|
||||
marks.add(new Mark(m.start(1), m.end(1), TokenType.CLASS_KEYWORD));
|
||||
marks.add(new Mark(m.start(1), m.end(1), TokenType.KEYWORD));
|
||||
|
||||
int nameStart = m.start(2);
|
||||
int nameEnd = m.end(2);
|
||||
@@ -868,7 +868,7 @@ public class JavaTextContainer extends TextContainer {
|
||||
|
||||
// Highlight general keywords
|
||||
collectPatternMatches(marks, KEYWORD, TokenType.KEYWORD);
|
||||
collectPatternMatches(marks, MODIFIER, TokenType.MODIFIER);
|
||||
collectPatternMatches(marks, MODIFIER, TokenType.KEYWORD);
|
||||
|
||||
collectTypeDeclarations(marks);
|
||||
collectPatternMatches(marks, NEW_TYPE, TokenType.NEW_TYPE, 1);
|
||||
@@ -1211,10 +1211,7 @@ public class JavaTextContainer extends TextContainer {
|
||||
public enum TokenType {
|
||||
COMMENT('7', 130),
|
||||
STRING('5', 120),
|
||||
CLASS_KEYWORD('c', 115),
|
||||
IMPORT_KEYWORD('6', 110),
|
||||
KEYWORD('c', 100),
|
||||
MODIFIER('6', 90),
|
||||
NEW_TYPE('d', 80),
|
||||
IMPORTED_CLASS('3', 75), //imported classes used in code
|
||||
INTERFACE_DECL('b', 85),
|
||||
|
||||
@@ -23,9 +23,9 @@ public class ScrollState {
|
||||
private boolean clickScrolling = false;
|
||||
|
||||
// Animation parameters
|
||||
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
|
||||
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 (increased to avoid microstutters)
|
||||
private static final double MAX_DT = 0.05; // Max delta time for stability
|
||||
|
||||
/**
|
||||
* Reset scroll state to initial values
|
||||
|
||||
+23
-9
@@ -1,5 +1,7 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.weighter.CompletionWeigher;
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.weighter.WeigherChain;
|
||||
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;
|
||||
@@ -22,12 +24,13 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
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
|
||||
FIELD(10), // Field or property
|
||||
PARAMETER(19), // Local variable or parameter
|
||||
VARIABLE(20), // Local variable or parameter
|
||||
CLASS(30), // Class or interface type
|
||||
ENUM(40), // Enum type
|
||||
KEYWORD(60), // Language keyword
|
||||
SNIPPET(70); // Code snippet
|
||||
|
||||
private final int priority;
|
||||
|
||||
@@ -141,7 +144,7 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
signature,
|
||||
method.getDocumentation(),
|
||||
method,
|
||||
false, // TODO: Check for @Deprecated annotation
|
||||
method.getJavaMethod() != null && method.getJavaMethod().isAnnotationPresent(Deprecated.class),
|
||||
false, // Methods don't require imports
|
||||
null,
|
||||
-1 // Java methods don't use inheritance depth sorting
|
||||
@@ -158,7 +161,7 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
Kind kind;
|
||||
switch (field.getScope()) {
|
||||
case PARAMETER:
|
||||
kind = Kind.VARIABLE;
|
||||
kind = Kind.PARAMETER;
|
||||
break;
|
||||
case LOCAL:
|
||||
kind = Kind.VARIABLE;
|
||||
@@ -577,8 +580,12 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a boost to the match score (e.g., from usage tracking).
|
||||
* @deprecated Use {@link WeigherChain} instead. This method is no longer
|
||||
* called by the autocomplete system. Score boosts are now handled by
|
||||
* individual {@link CompletionWeigher} implementations that return
|
||||
* independent comparable values per scoring dimension.
|
||||
*/
|
||||
@Deprecated
|
||||
public void addScoreBoost(int boost) {
|
||||
this.matchScore += boost;
|
||||
}
|
||||
@@ -599,6 +606,7 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
public String getImportPath() { return importPath; }
|
||||
public int getMatchScore() { return matchScore; }
|
||||
public int[] getMatchIndices() { return matchIndices; }
|
||||
public int getInheritanceDepth() { return inheritanceDepth; }
|
||||
public int getColor() { return color; };
|
||||
public AutocompleteItem setColor(int col){ this.color = col;return this; }
|
||||
/**
|
||||
@@ -694,6 +702,7 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
case METHOD: return "m";
|
||||
case FIELD: return "f";
|
||||
case VARIABLE: return "v";
|
||||
case PARAMETER: return "p";
|
||||
case CLASS: return "C";
|
||||
case ENUM: return "E";
|
||||
case ENUM_CONSTANT: return "e";
|
||||
@@ -711,6 +720,7 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
case METHOD: return 0xFFB877DB; // Purple for methods
|
||||
case FIELD: return 0xFF79C0FF; // Blue for fields
|
||||
case VARIABLE: return 0xFF7EE787; // Green for variables
|
||||
case PARAMETER: return 0XFFFF00FF; // Green for variables
|
||||
case CLASS: return 0xFFFFA657; // Orange for classes
|
||||
case ENUM: return 0xFFFFD866; // Yellow for enums
|
||||
case ENUM_CONSTANT: return 0xFFFFD866;
|
||||
@@ -720,6 +730,10 @@ public class AutocompleteItem implements Comparable<AutocompleteItem> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: No longer used for autocomplete sorting (weigher chain used instead).
|
||||
* Kept for backward compatibility and collections sorting.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(AutocompleteItem other) {
|
||||
// First by match score (descending) - this includes all penalties and boosts
|
||||
|
||||
+33
-5
@@ -165,7 +165,7 @@ public class AutocompleteManager {
|
||||
*/
|
||||
public void onCharTyped(char c, String text, int cursorPosition) {
|
||||
// Check if we should close autocomplete
|
||||
if (CLOSE_CHARS.indexOf(c) >= 0) {
|
||||
if (CLOSE_CHARS.indexOf(c) >= 0 || document.isExcludedInclusive(cursorPosition)) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
@@ -389,6 +389,17 @@ public class AutocompleteManager {
|
||||
|
||||
// Walk left across a dotted receiver chain
|
||||
while (pos >= 0) {
|
||||
// Skip excluded regions (comments/strings) to avoid reading dots from inside them
|
||||
if (document != null && document.isExcluded(pos)) {
|
||||
int jumped = jumpBeforeExcluded(pos);
|
||||
if (jumped == pos) {
|
||||
pos--;
|
||||
} else {
|
||||
pos = jumped;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char c = text.charAt(pos);
|
||||
|
||||
if (Character.isWhitespace(c)) {
|
||||
@@ -414,10 +425,24 @@ public class AutocompleteManager {
|
||||
// Consume identifier
|
||||
while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) pos--;
|
||||
|
||||
// If preceded by a dot, keep going
|
||||
// If preceded by a dot, keep going (skip excluded ranges when looking for the dot)
|
||||
int checkPos = pos;
|
||||
while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) {
|
||||
checkPos--;
|
||||
while (checkPos >= 0) {
|
||||
char ch = text.charAt(checkPos);
|
||||
if (Character.isWhitespace(ch)) {
|
||||
checkPos--;
|
||||
continue;
|
||||
}
|
||||
if (document != null && document.isExcluded(checkPos)) {
|
||||
int jumped = jumpBeforeExcluded(checkPos);
|
||||
if (jumped == checkPos) {
|
||||
checkPos--;
|
||||
} else {
|
||||
checkPos = jumped;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (checkPos >= 0 && text.charAt(checkPos) == '.') {
|
||||
pos = checkPos - 1;
|
||||
@@ -1044,13 +1069,16 @@ public class AutocompleteManager {
|
||||
|
||||
// Check if we found a dot
|
||||
if (pos >= 0 && text.charAt(pos) == '.') {
|
||||
if (document != null && document.isExcluded(pos)) return -1;
|
||||
return pos;
|
||||
} else {
|
||||
while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos)))
|
||||
pos--;
|
||||
|
||||
if (pos >= 0 && text.charAt(pos) == '.')
|
||||
if (pos >= 0 && text.charAt(pos) == '.') {
|
||||
if (document != null && document.isExcluded(pos)) return -1;
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -419,7 +419,7 @@ public class AutocompleteMenu extends Gui {
|
||||
GL11.glPushMatrix();
|
||||
float scale = 0.5f;
|
||||
GL11.glScalef(scale, scale, scale);
|
||||
int col = TokenType.MODIFIER.getHexColor();
|
||||
int col = TokenType.KEYWORD.getHexColor();
|
||||
|
||||
if (isStatic)
|
||||
font.drawString("s", (int) (textX / scale), (int) (textY / scale), col);
|
||||
@@ -482,6 +482,8 @@ public class AutocompleteMenu extends Gui {
|
||||
return TokenType.getColor(item.getTypeInfo());
|
||||
case VARIABLE:
|
||||
return TokenType.LOCAL_FIELD.getHexColor();
|
||||
case PARAMETER:
|
||||
return TokenType.PARAMETER.getHexColor();
|
||||
case KEYWORD:
|
||||
return TokenType.KEYWORD.getHexColor();
|
||||
default:
|
||||
@@ -637,6 +639,9 @@ public class AutocompleteMenu extends Gui {
|
||||
|
||||
private void drawTypeLabel(AutocompleteItem item, String typeLabel, int x, int y, int typeColor, TypeInfo typeInfo) {
|
||||
if (typeInfo == null || item.getKind() == AutocompleteItem.Kind.CLASS) { //draw package on right
|
||||
// draw default package color instead of interface/enum colors
|
||||
if (typeInfo != null && !typeInfo.isClass())
|
||||
typeColor = TokenType.getPackageColor();
|
||||
drawSimpleTypeWithArraySuffix(typeLabel, x, y, typeColor);
|
||||
return;
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.weighter.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CompletionWeigherChains {
|
||||
|
||||
public static WeigherChain javaChain() {
|
||||
return new WeigherChain(Arrays.asList(
|
||||
new MatchQualityWeigher(),
|
||||
new MemberKindWeigher(),
|
||||
new ContextFitWeigher(),
|
||||
new ImportStatusWeigher(),
|
||||
new UsageFrequencyWeigher(),
|
||||
new DeprecatedWeigher(),
|
||||
new AlphabeticalWeigher()
|
||||
));
|
||||
}
|
||||
|
||||
public static WeigherChain jsChain() {
|
||||
return new WeigherChain(Arrays.asList(
|
||||
new MatchQualityWeigher(),
|
||||
new MemberKindWeigher(),
|
||||
new ContextFitWeigher(),
|
||||
new ImportStatusWeigher(),
|
||||
new UsageFrequencyWeigher(),
|
||||
new InheritanceDepthWeigher(),
|
||||
new DeprecatedWeigher(),
|
||||
new AlphabeticalWeigher()
|
||||
));
|
||||
}
|
||||
}
|
||||
+94
-87
@@ -1,15 +1,16 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.weighter.WeigherChain;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.*;
|
||||
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.synthetic.*;
|
||||
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.config.ConfigScript;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
|
||||
@@ -37,6 +38,15 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider {
|
||||
/**
|
||||
* Add suggestions for member access (after dot).
|
||||
* Uses ScriptDocument's unified type resolution system.
|
||||
*
|
||||
* Fixes applied vs original implementation:
|
||||
* - Single getMemberAccessResolvePosition() call (was duplicated)
|
||||
* - Single getMergeRegistryType() call (was duplicated)
|
||||
* - Shared addedMethods/addedFields sets across ALL sources for cross-source deduplication
|
||||
* - Unified nested type lookup via getAllNestedTypes() instead of raw reflection
|
||||
* - Single Object method injection path (registry-based via fromJSMethod)
|
||||
* - Name-based Object.class guard instead of fragile Class identity comparison
|
||||
* - Logged SecurityException instead of silently swallowed
|
||||
*/
|
||||
protected void addMemberSuggestions(Context context, List<AutocompleteItem> items) {
|
||||
String receiverExpr = context.receiverExpression;
|
||||
@@ -44,113 +54,105 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve position once — used for both type resolution and static context detection
|
||||
int resolvePos = getMemberAccessResolvePosition(context);
|
||||
|
||||
// Use ScriptDocument's resolveExpressionType - handles both Java and JS
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, resolvePos);
|
||||
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());
|
||||
// --- JS-SPECIFIC: synthetic type early return ---
|
||||
SyntheticType syntheticType = document.getTypeResolver().getSyntheticType(receiverType.getSimpleName());
|
||||
if (syntheticType != null) {
|
||||
addSyntheticTypeSuggestions(syntheticType, items, context.methodsOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
boolean isStaticContext = isStaticAccess(receiverExpr, resolvePos);
|
||||
|
||||
// Resolve merge-registry type once — used for both method and field fallback
|
||||
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));
|
||||
}
|
||||
JSTypeInfo mergeRegistryType = (javaClass != null
|
||||
&& receiverType.getSyntheticMethods().isEmpty()
|
||||
&& receiverType.getSyntheticFields().isEmpty())
|
||||
? getMergeRegistryType(javaClass) : null;
|
||||
|
||||
// Shared deduplication sets — prevent the same method/field appearing from multiple sources
|
||||
Set<String> addedMethods = new HashSet<>();
|
||||
Set<String> addedFields = new HashSet<>();
|
||||
|
||||
// ---- SHARED: unified method enumeration (TypeInfo.getAllMethods covers reflection + synthetics) ----
|
||||
for (MethodInfo method : receiverType.getAllMethods()) {
|
||||
if (isStaticContext && !Modifier.isStatic(method.getModifiers()))
|
||||
continue;
|
||||
String sig = method.getName() + "(" + method.getParameterCount() + ")";
|
||||
if (addedMethods.add(sig))
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
}
|
||||
|
||||
// --- JS-SPECIFIC: merge registry type fallback for methods ---
|
||||
if (mergeRegistryType != null) {
|
||||
addMethodsFromType(mergeRegistryType, receiverType, items, addedMethods, context.methodsOnly,
|
||||
isStaticContext);
|
||||
}
|
||||
|
||||
if (!context.methodsOnly) {
|
||||
// ---- SHARED: unified field enumeration with deduplication ----
|
||||
for (FieldInfo field : receiverType.getAllFields()) {
|
||||
if (isStaticContext && !Modifier.isStatic(field.getModifiers()))
|
||||
continue;
|
||||
if (addedFields.add(field.getName()))
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// --- JS-SPECIFIC: merge registry type fallback for fields ---
|
||||
if (mergeRegistryType != null) {
|
||||
addFieldsFromType(mergeRegistryType, receiverType, items, addedFields, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- SHARED: nested types (inner classes) in static context via getAllNestedTypes() ----
|
||||
if (isStaticContext && !context.methodsOnly) {
|
||||
// ScriptTypeInfo.getAllNestedTypes() returns script inner classes directly;
|
||||
// TypeInfo.getAllNestedTypes() uses reflection. No duplicate logic needed.
|
||||
for (TypeInfo nestedType : receiverType.getAllNestedTypes()) {
|
||||
items.add(AutocompleteItem.fromType(nestedType));
|
||||
}
|
||||
if (!isStaticContext && javaClass != Object.class) {
|
||||
JSTypeInfo objJSType = registry.getType("Object");
|
||||
if (objJSType != null) {
|
||||
for (JSMethodInfo m : objJSType.getMethods().values()) {
|
||||
if (!m.isStatic()) {
|
||||
}
|
||||
|
||||
// Enum constants — polymorphic via TypeInfo.getEnumConstants()
|
||||
if (!context.methodsOnly) {
|
||||
for (EnumConstantInfo enumConstant : receiverType.getEnumConstants().values()) {
|
||||
items.add(AutocompleteItem.fromField(enumConstant.getFieldInfo()));
|
||||
}
|
||||
}
|
||||
|
||||
// --- JS-SPECIFIC: Object methods from JS registry (single unified path) ---
|
||||
// Add Object.class JS methods for instance access, unless receiver IS Object itself.
|
||||
// Uses name comparison instead of Class identity for robustness across classloaders.
|
||||
if (!isStaticContext && (javaClass == null || !"java.lang.Object".equals(javaClass.getName()))) {
|
||||
JSTypeInfo objJSType = registry.getType("Object");
|
||||
if (objJSType != null) {
|
||||
for (JSMethodInfo m : objJSType.getMethods().values()) {
|
||||
if (!m.isStatic()) {
|
||||
String sig = m.getName() + "(" + m.getParameterCount() + ")";
|
||||
if (addedMethods.add(sig)) {
|
||||
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));
|
||||
}
|
||||
// --- JS-SPECIFIC: pure JS type fallback (javaClass == null, not ScriptTypeInfo) ---
|
||||
if (javaClass == null && !(receiverType instanceof ScriptTypeInfo)) {
|
||||
JSTypeInfo jsTypeInfo = receiverType.getJSTypeInfo();
|
||||
if (jsTypeInfo != null) {
|
||||
addMethodsFromType(jsTypeInfo, receiverType, items, addedMethods, context.methodsOnly, isStaticContext);
|
||||
if (!context.methodsOnly && ConfigScript.ShowImplementationFieldsInAutocomplete) {
|
||||
addFieldsFromType(jsTypeInfo, receiverType, items, addedFields, isStaticContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -348,6 +350,11 @@ public class JSAutocompleteProvider extends JavaAutocompleteProvider {
|
||||
return UsageTracker.getJSInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WeigherChain getWeigherChain() {
|
||||
return CompletionWeigherChains.jsChain();
|
||||
}
|
||||
|
||||
public String[] getKeywords() {
|
||||
return TypeChecker.getJavaScriptKeywords();
|
||||
}
|
||||
|
||||
+46
-256
@@ -1,5 +1,7 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.weighter.ScoringContext;
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.weighter.WeigherChain;
|
||||
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;
|
||||
@@ -10,8 +12,6 @@ import noppes.npcs.client.gui.util.script.interpreter.type.TypeChecker;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeResolver;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.*;
|
||||
|
||||
@@ -83,12 +83,9 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter and score by prefix, then apply usage boosts and static penalties
|
||||
// Filter by prefix, then sort using weigher chain
|
||||
filterAndScore(items, context.prefix, context.isMemberAccess, isStaticContext, ownerFullName);
|
||||
|
||||
// Sort by score
|
||||
Collections.sort(items);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -100,92 +97,52 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
if (receiverExpr == null || receiverExpr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve position once — used for both type resolution and static context detection
|
||||
int resolvePos = getMemberAccessResolvePosition(context);
|
||||
|
||||
// Resolve the type of the receiver expression
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
TypeInfo receiverType = document.resolveExpressionType(receiverExpr, resolvePos);
|
||||
|
||||
if (receiverType == null || !receiverType.isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if this is a static context (accessing a class type)
|
||||
boolean isStaticContext = isStaticAccess(receiverExpr, getMemberAccessResolvePosition(context));
|
||||
boolean isStaticContext = isStaticAccess(receiverExpr, resolvePos);
|
||||
|
||||
Class<?> clazz = receiverType.getJavaClass();
|
||||
if (clazz == null) {
|
||||
// Try ScriptTypeInfo
|
||||
if (receiverType instanceof ScriptTypeInfo) {
|
||||
addScriptTypeMembers((ScriptTypeInfo) receiverType, items, isStaticContext, context.methodsOnly);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Add methods
|
||||
// Shared deduplication sets across all member sources
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<String> addedFields = new HashSet<>();
|
||||
|
||||
for (MethodInfo method : receiverType.getSyntheticMethods()) {
|
||||
if (isStaticContext && !Modifier.isStatic(method.getModifiers()))
|
||||
continue;
|
||||
|
||||
items.add(AutocompleteItem.fromMethod(method, context.methodsOnly));
|
||||
// Unified method enumeration — works for Java types, ScriptTypeInfo, and intersection bounds
|
||||
for (MethodInfo method : receiverType.getAllMethods()) {
|
||||
if (isStaticContext && !Modifier.isStatic(method.getModifiers())) continue;
|
||||
String sig = method.getName() + "(" + method.getParameterCount() + ")";
|
||||
if (addedMethods.add(sig))
|
||||
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));
|
||||
// Unified field enumeration with deduplication
|
||||
for (FieldInfo field : receiverType.getAllFields()) {
|
||||
if (isStaticContext && !Modifier.isStatic(field.getModifiers())) continue;
|
||||
if (addedFields.add(field.getName()))
|
||||
items.add(AutocompleteItem.fromField(field));
|
||||
}
|
||||
}
|
||||
|
||||
// Add nested types (inner classes/interfaces) in static context
|
||||
// Nested types (inner classes) in static context via getAllNestedTypes()
|
||||
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()
|
||||
for (TypeInfo nestedType : receiverType.getAllNestedTypes()) {
|
||||
items.add(AutocompleteItem.fromType(nestedType));
|
||||
}
|
||||
}
|
||||
|
||||
// Enum constants — polymorphic via TypeInfo.getEnumConstants()
|
||||
if (!context.methodsOnly) {
|
||||
for (EnumConstantInfo enumConstant : receiverType.getEnumConstants().values()) {
|
||||
items.add(AutocompleteItem.fromField(enumConstant.getFieldInfo()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,76 +175,6 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
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).
|
||||
*/
|
||||
@@ -427,8 +314,6 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
if (existingSimpleNames.contains(type.getSimpleName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create an item that requires import
|
||||
AutocompleteItem item = new AutocompleteItem.Builder()
|
||||
.name(type.getSimpleName())
|
||||
.insertText(type.getSimpleName())
|
||||
@@ -483,126 +368,31 @@ public class JavaAutocompleteProvider implements AutocompleteProvider {
|
||||
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,
|
||||
protected WeigherChain getWeigherChain() {
|
||||
return CompletionWeigherChains.javaChain();
|
||||
}
|
||||
|
||||
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);
|
||||
int score = item.calculateMatchScore(
|
||||
prefix != null ? 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;
|
||||
}
|
||||
ScoringContext context = new ScoringContext(
|
||||
prefix, isMemberAccess, isStaticContext,
|
||||
ownerFullName, getUsageTracker(), requirePrefix
|
||||
);
|
||||
|
||||
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
|
||||
WeigherChain weigherChain = getWeigherChain();
|
||||
Comparator<AutocompleteItem> comparator = weigherChain.buildComparator(context);
|
||||
items.sort(comparator);
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ public class UsageTracker {
|
||||
|
||||
// ==================== INSTANCE METHODS ====================
|
||||
|
||||
private UsageTracker(String filename) {
|
||||
UsageTracker(String filename) {
|
||||
File dir = getDir();
|
||||
this.file = new File(dir, filename);
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
public class AlphabeticalWeigher extends CompletionWeigher {
|
||||
|
||||
public AlphabeticalWeigher() {
|
||||
super("alphabetical");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
return item.getName().toLowerCase();
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
/**
|
||||
* A single scoring dimension for autocomplete ranking.
|
||||
*
|
||||
* Weighers are evaluated in chain order. For each pair of items being compared,
|
||||
* the first weigher to return non-equal values determines the ordering.
|
||||
*
|
||||
* Contract:
|
||||
* - weigh() MUST be a pure function (no side effects, no mutations)
|
||||
* - weigh() MUST return consistent results for the same item+context
|
||||
* - weigh() MAY return null to abstain (treated as equal for this weigher)
|
||||
* - Lower Comparable values = higher priority (shown first) unless negated
|
||||
*
|
||||
* Modeled after IntelliJ's LookupElementWeigher (Apache 2.0).
|
||||
*/
|
||||
public abstract class CompletionWeigher {
|
||||
|
||||
private final String id;
|
||||
private final boolean negated;
|
||||
|
||||
protected CompletionWeigher(String id, boolean negated) {
|
||||
this.id = id;
|
||||
this.negated = negated;
|
||||
}
|
||||
|
||||
protected CompletionWeigher(String id) {
|
||||
this(id, false);
|
||||
}
|
||||
|
||||
public abstract Comparable<?> weigh(AutocompleteItem item, ScoringContext context);
|
||||
|
||||
public String getId() { return id; }
|
||||
public boolean isNegated() { return negated; }
|
||||
|
||||
@Override
|
||||
public String toString() { return id; }
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
/**
|
||||
* How well does this item fit the current access context?
|
||||
*
|
||||
* Combines what were previously applyStaticPenalty() and applyObjectMethodPenalty().
|
||||
* Uses ordered enum categories, eliminating all magic threshold numbers.
|
||||
*/
|
||||
public class ContextFitWeigher extends CompletionWeigher {
|
||||
|
||||
public enum ContextFit {
|
||||
/** Item naturally belongs in this context. */
|
||||
NATURAL,
|
||||
/** Static member accessed through an instance — legal but bad style. */
|
||||
WRONG_STATIC_CONTEXT,
|
||||
/** Inherited Object method (wait, notify, hashCode, getClass, etc.) */
|
||||
OBJECT_METHOD
|
||||
}
|
||||
|
||||
public ContextFitWeigher() {
|
||||
super("contextFit");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
if (item.isInheritedObjectMethod()) {
|
||||
return ContextFit.OBJECT_METHOD;
|
||||
}
|
||||
|
||||
if (context.isMemberAccess && !context.isStaticContext && item.isStatic()) {
|
||||
return ContextFit.WRONG_STATIC_CONTEXT;
|
||||
}
|
||||
|
||||
return ContextFit.NATURAL;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
public class DeprecatedWeigher extends CompletionWeigher {
|
||||
|
||||
public DeprecatedWeigher() {
|
||||
super("deprecated");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
return item.isDeprecated();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
/**
|
||||
* Demote unimported classes below imported ones.
|
||||
*
|
||||
* Fixes the bug where the unimported class penalty (-500) was applied at
|
||||
* creation time and overwritten by calculateMatchScore(). In the chain model,
|
||||
* import status is a separate dimension that can never be overwritten.
|
||||
*/
|
||||
public class ImportStatusWeigher extends CompletionWeigher {
|
||||
|
||||
public enum ImportStatus {
|
||||
IMPORTED,
|
||||
UNIMPORTED
|
||||
}
|
||||
|
||||
public ImportStatusWeigher() {
|
||||
super("importStatus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
return item.requiresImport() ? ImportStatus.UNIMPORTED : ImportStatus.IMPORTED;
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
/**
|
||||
* Prefer members defined closer in the inheritance tree.
|
||||
* Child class members rank above parent class members.
|
||||
*
|
||||
* Only meaningful for JS/.d.ts types where inheritance depth is tracked.
|
||||
* For items without inheritance tracking (depth < 0), this weigher abstains.
|
||||
*/
|
||||
public class InheritanceDepthWeigher extends CompletionWeigher {
|
||||
|
||||
public InheritanceDepthWeigher() {
|
||||
super("inheritanceDepth");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
int depth = item.getInheritanceDepth();
|
||||
if (depth < 0) {
|
||||
return null;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
/**
|
||||
* Text match quality against the user's prefix.
|
||||
* PRIMARY sort dimension — how well the item name matches what was typed.
|
||||
*
|
||||
* Returns the existing calculateMatchScore() result, negated so that
|
||||
* higher scores = higher priority.
|
||||
*/
|
||||
public class MatchQualityWeigher extends CompletionWeigher {
|
||||
|
||||
public MatchQualityWeigher() {
|
||||
super("matchQuality", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
return item.getMatchScore();
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.TypeInfo;
|
||||
|
||||
public class MemberKindWeigher extends CompletionWeigher {
|
||||
|
||||
public enum MemberKind {
|
||||
ENUM_CONSTANT,
|
||||
LOCAL_VARIABLE,
|
||||
INSTANCE_FIELD,
|
||||
INSTANCE_METHOD,
|
||||
TYPE_CLASS,
|
||||
TYPE_ENUM,
|
||||
TYPE_INTERFACE,
|
||||
STATIC_FIELD,
|
||||
STATIC_METHOD,
|
||||
KEYWORD,
|
||||
SNIPPET
|
||||
}
|
||||
|
||||
public MemberKindWeigher() {
|
||||
super("memberKind");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
switch (item.getKind()) {
|
||||
case ENUM_CONSTANT: return MemberKind.ENUM_CONSTANT;
|
||||
case PARAMETER: return MemberKind.LOCAL_VARIABLE;
|
||||
case VARIABLE: return MemberKind.LOCAL_VARIABLE;
|
||||
case METHOD:
|
||||
return item.isStatic() ? MemberKind.STATIC_METHOD : MemberKind.INSTANCE_METHOD;
|
||||
case FIELD:
|
||||
return item.isStatic() ? MemberKind.STATIC_FIELD : MemberKind.INSTANCE_FIELD;
|
||||
case CLASS: return resolveTypeTier(item);
|
||||
case ENUM: return MemberKind.TYPE_ENUM;
|
||||
case KEYWORD: return MemberKind.KEYWORD;
|
||||
case SNIPPET: return MemberKind.SNIPPET;
|
||||
default: return MemberKind.INSTANCE_FIELD;
|
||||
}
|
||||
}
|
||||
|
||||
private MemberKind resolveTypeTier(AutocompleteItem item) {
|
||||
Object source = item.getSourceData();
|
||||
if (source instanceof TypeInfo) {
|
||||
TypeInfo typeInfo = (TypeInfo) source;
|
||||
switch (typeInfo.getKind()) {
|
||||
case INTERFACE: return MemberKind.TYPE_INTERFACE;
|
||||
case ENUM: return MemberKind.TYPE_ENUM;
|
||||
default: return MemberKind.TYPE_CLASS;
|
||||
}
|
||||
}
|
||||
return MemberKind.TYPE_CLASS;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.UsageTracker;
|
||||
|
||||
/**
|
||||
* Immutable context for a single scoring pass.
|
||||
* Created once when suggestions are requested, shared by all weighers.
|
||||
*/
|
||||
public class ScoringContext {
|
||||
|
||||
public final String prefix;
|
||||
public final boolean isMemberAccess;
|
||||
public final boolean isStaticContext;
|
||||
public final String ownerFullName;
|
||||
public final UsageTracker usageTracker;
|
||||
public final boolean requirePrefix;
|
||||
|
||||
public ScoringContext(String prefix, boolean isMemberAccess, boolean isStaticContext,
|
||||
String ownerFullName, UsageTracker usageTracker, boolean requirePrefix) {
|
||||
this.prefix = prefix;
|
||||
this.isMemberAccess = isMemberAccess;
|
||||
this.isStaticContext = isStaticContext;
|
||||
this.ownerFullName = ownerFullName;
|
||||
this.usageTracker = usageTracker;
|
||||
this.requirePrefix = requirePrefix;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.UsageTracker;
|
||||
|
||||
/**
|
||||
* Boost items the user has selected before.
|
||||
* Uses logarithmic scaling with a cap via UsageTracker.calculateUsageBoost().
|
||||
* Negated: higher usage count = higher priority.
|
||||
*/
|
||||
public class UsageFrequencyWeigher extends CompletionWeigher {
|
||||
|
||||
public UsageFrequencyWeigher() {
|
||||
super("usageFrequency", true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> weigh(AutocompleteItem item, ScoringContext context) {
|
||||
if (context.usageTracker == null) {
|
||||
return 0;
|
||||
}
|
||||
int usageCount = context.usageTracker.getUsageCount(item, context.ownerFullName);
|
||||
return UsageTracker.calculateUsageBoost(usageCount);
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package noppes.npcs.client.gui.util.script.autocomplete.weighter;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.autocomplete.AutocompleteItem;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* An ordered list of weighers that produces a Comparator for sorting autocomplete items.
|
||||
*
|
||||
* Items are compared dimension-by-dimension through the chain.
|
||||
* The first weigher to produce a non-equal comparison determines the ordering.
|
||||
* If all weighers return equal, items are considered equivalent (stable sort preserves
|
||||
* insertion order).
|
||||
*
|
||||
* Simplified from IntelliJ's CompletionSorterImpl — uses Comparator instead of a
|
||||
* classifier tree since we sort all items in a single batch.
|
||||
*/
|
||||
public class WeigherChain {
|
||||
|
||||
private final List<CompletionWeigher> weighers;
|
||||
|
||||
public WeigherChain(List<CompletionWeigher> weighers) {
|
||||
this.weighers = Collections.unmodifiableList(new ArrayList<>(weighers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Comparator that evaluates items through the weigher chain.
|
||||
*
|
||||
* Weigher results are cached per item via IdentityHashMap to avoid
|
||||
* recomputing weights during O(N log N) comparisons.
|
||||
*/
|
||||
public Comparator<AutocompleteItem> buildComparator(ScoringContext context) {
|
||||
final Map<AutocompleteItem, Comparable<?>[]> cache = new IdentityHashMap<>();
|
||||
final int chainLength = weighers.size();
|
||||
|
||||
return (a, b) -> {
|
||||
Comparable<?>[] weightsA = cache.computeIfAbsent(a, k -> computeWeights(k, context));
|
||||
Comparable<?>[] weightsB = cache.computeIfAbsent(b, k -> computeWeights(k, context));
|
||||
|
||||
for (int i = 0; i < chainLength; i++) {
|
||||
Comparable<?> wA = weightsA[i];
|
||||
Comparable<?> wB = weightsB[i];
|
||||
|
||||
if (wA == null && wB == null) continue;
|
||||
if (wA == null) return 1;
|
||||
if (wB == null) return -1;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
int cmp = ((Comparable<Object>) wA).compareTo(wB);
|
||||
|
||||
if (cmp != 0) {
|
||||
return weighers.get(i).isNegated() ? -cmp : cmp;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
}
|
||||
|
||||
private Comparable<?>[] computeWeights(AutocompleteItem item, ScoringContext context) {
|
||||
Comparable<?>[] weights = new Comparable<?>[weighers.size()];
|
||||
for (int i = 0; i < weighers.size(); i++) {
|
||||
weights[i] = weighers.get(i).weigh(item, context);
|
||||
}
|
||||
return weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all weigher results for an item (for debugging/testing).
|
||||
*/
|
||||
public Map<String, Comparable<?>> debugWeights(AutocompleteItem item, ScoringContext context) {
|
||||
Map<String, Comparable<?>> result = new LinkedHashMap<>();
|
||||
for (CompletionWeigher weigher : weighers) {
|
||||
result.put(weigher.getId(), weigher.weigh(item, context));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<CompletionWeigher> getWeighers() {
|
||||
return weighers;
|
||||
}
|
||||
}
|
||||
+47
-13
@@ -44,19 +44,32 @@ public class ErrorUnderlineRenderer {
|
||||
ScriptDocument doc,
|
||||
int lineStartX, int baselineY,
|
||||
String lineText, int lineStart, int lineEnd) {
|
||||
drawErrorUnderlines(doc, null, lineStartX, baselineY, lineText, lineStart, lineEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw error underlines for all validation errors in the document that intersect the given line.
|
||||
* When a ScriptLine is provided, uses style-aware width calculation so underlines align
|
||||
* correctly under bold/italic tokens.
|
||||
*/
|
||||
public static void drawErrorUnderlines(
|
||||
ScriptDocument doc, ScriptLine line,
|
||||
int lineStartX, int baselineY,
|
||||
String lineText, int lineStart, int lineEnd) {
|
||||
|
||||
if (doc == null)
|
||||
return;
|
||||
|
||||
for (DocumentError error : doc.getErrors()) {
|
||||
drawUnderlineForSpan(error.getStartPos(), error.getEndPos(),
|
||||
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR);
|
||||
lineStartX, baselineY, lineText, lineStart, lineEnd, ERROR_COLOR, line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate underline position for a span of text within a line.
|
||||
* Handles clipping to line boundaries and pixel width calculation.
|
||||
* When a ScriptLine is provided, uses style-aware width calculation.
|
||||
*
|
||||
* @param spanStart Global offset where the span starts
|
||||
* @param spanEnd Global offset where the span ends
|
||||
@@ -64,11 +77,13 @@ public class ErrorUnderlineRenderer {
|
||||
* @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 line Optional ScriptLine for style-aware width; null falls back to plain width
|
||||
* @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) {
|
||||
int lineStartX, String lineText, int lineStart, int lineEnd,
|
||||
ScriptLine line) {
|
||||
|
||||
// Skip if span doesn't intersect this line
|
||||
if (spanEnd < lineStart || spanStart > lineEnd)
|
||||
@@ -89,18 +104,24 @@ public class ErrorUnderlineRenderer {
|
||||
if (lineLocalStart < 0 || lineLocalStart >= lineText.length())
|
||||
return null;
|
||||
|
||||
// Compute pixel position
|
||||
String beforeSpan = lineText.substring(0, lineLocalStart);
|
||||
int beforeWidth = ClientProxy.Font.width(beforeSpan);
|
||||
|
||||
// Compute pixel position using style-aware width when available
|
||||
int beforeWidth;
|
||||
int spanWidth;
|
||||
if (lineLocalEnd > lineText.length()) {
|
||||
// Span extends past line end
|
||||
spanWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
|
||||
|
||||
if (line != null) {
|
||||
beforeWidth = line.getRenderedWidth(0, lineLocalStart);
|
||||
int clampedEnd = Math.min(lineLocalEnd, lineText.length());
|
||||
spanWidth = line.getRenderedWidth(lineLocalStart, clampedEnd);
|
||||
} else {
|
||||
// Span is fully on this line (or clipped)
|
||||
String spanTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
|
||||
spanWidth = ClientProxy.Font.width(spanTextOnLine);
|
||||
String beforeSpan = lineText.substring(0, lineLocalStart);
|
||||
beforeWidth = ClientProxy.Font.width(beforeSpan);
|
||||
|
||||
if (lineLocalEnd > lineText.length()) {
|
||||
spanWidth = ClientProxy.Font.width(lineText.substring(lineLocalStart));
|
||||
} else {
|
||||
String spanTextOnLine = lineText.substring(lineLocalStart, lineLocalEnd);
|
||||
spanWidth = ClientProxy.Font.width(spanTextOnLine);
|
||||
}
|
||||
}
|
||||
|
||||
return new UnderlinePosition(lineStartX + beforeWidth, spanWidth);
|
||||
@@ -123,9 +144,22 @@ public class ErrorUnderlineRenderer {
|
||||
int lineStartX, int baselineY,
|
||||
String lineText, int lineStart, int lineEnd,
|
||||
int color) {
|
||||
drawUnderlineForSpan(spanStart, spanEnd, lineStartX, baselineY,
|
||||
lineText, lineStart, lineEnd, color, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw an underline for a simple span if it intersects the line.
|
||||
* When a ScriptLine is provided, uses style-aware width calculation.
|
||||
*/
|
||||
public static void drawUnderlineForSpan(
|
||||
int spanStart, int spanEnd,
|
||||
int lineStartX, int baselineY,
|
||||
String lineText, int lineStart, int lineEnd,
|
||||
int color, ScriptLine line) {
|
||||
|
||||
UnderlinePosition pos = calculateUnderlinePosition(
|
||||
spanStart, spanEnd, lineStartX, lineText, lineStart, lineEnd);
|
||||
spanStart, spanEnd, lineStartX, lineText, lineStart, lineEnd, line);
|
||||
|
||||
if (pos != null && pos.isValid()) {
|
||||
drawCurlyUnderline(pos.x, baselineY, pos.width, color);
|
||||
|
||||
@@ -151,11 +151,11 @@ public class FieldChainMarker {
|
||||
int pos = m.end(2);
|
||||
while (pos < text.length()) {
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || text.charAt(pos) != '.') break;
|
||||
if (pos >= text.length() || document.isExcluded(pos) || text.charAt(pos) != '.') break;
|
||||
pos++; // Skip dot
|
||||
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
|
||||
if (pos >= text.length() || document.isExcluded(pos) || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
|
||||
|
||||
// Read identifier
|
||||
int identStart = pos;
|
||||
@@ -452,11 +452,11 @@ public class FieldChainMarker {
|
||||
int pos = identEnd;
|
||||
while (pos < text.length()) {
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || text.charAt(pos) != '.') break;
|
||||
if (pos >= text.length() || document.isExcluded(pos) || text.charAt(pos) != '.') break;
|
||||
pos++; // Skip dot
|
||||
|
||||
pos = document.skipWhitespace(pos);
|
||||
if (pos >= text.length() || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
|
||||
if (pos >= text.length() || document.isExcluded(pos) || !Character.isJavaIdentifierStart(text.charAt(pos))) break;
|
||||
|
||||
// Read identifier
|
||||
int nStart = pos;
|
||||
|
||||
@@ -4308,6 +4308,54 @@ public class ScriptDocument {
|
||||
return resolveTypeAndTrackUsage(typeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively resolve any unresolved type names inside a TypeInfo using position-aware resolution.
|
||||
*
|
||||
* This handles a different concern than {@link GenericContext#substitute(TypeInfo)} —
|
||||
* it does not replace type variables with concrete bindings. Instead, it re-resolves
|
||||
* unresolved simple names (e.g., inner class names like "Node" or script-declared type
|
||||
* parameters like "T") that appear as type arguments in parameterized types.
|
||||
*
|
||||
* For example, if a type resolves to {@code List<Node>} but "Node" is still unresolved,
|
||||
* this will attempt to resolve "Node" using the position-aware
|
||||
* {@link #resolveType(String, int)} which can find inner classes and type parameters
|
||||
* visible at the given position.
|
||||
*
|
||||
* @param type the type whose unresolved components should be re-resolved
|
||||
* @param position the script text offset for scope-aware resolution
|
||||
* @return the type with all resolvable components resolved, or the original if nothing changed
|
||||
*/
|
||||
private TypeInfo substituteTypeParams(TypeInfo type, int position) {
|
||||
if (type == null)
|
||||
return null;
|
||||
|
||||
if (!type.isResolved() && type.getSimpleName() != null
|
||||
&& !type.getSimpleName().contains(".")) {
|
||||
TypeInfo sub = resolveType(type.getSimpleName(), position);
|
||||
return (sub != null && sub.isResolved()) ? sub : type;
|
||||
}
|
||||
|
||||
if (type.isParameterized()) {
|
||||
List<TypeInfo> args = type.getAppliedTypeArgs();
|
||||
if (args != null && !args.isEmpty()) {
|
||||
List<TypeInfo> substituted = new ArrayList<>(args.size());
|
||||
boolean anyChanged = false;
|
||||
for (TypeInfo arg : args) {
|
||||
TypeInfo sub = substituteTypeParams(arg, position);
|
||||
if (sub != arg)
|
||||
anyChanged = true;
|
||||
substituted.add(sub);
|
||||
}
|
||||
if (anyChanged) {
|
||||
TypeInfo raw = type.getRawType();
|
||||
return raw != null ? raw.parameterize(substituted) : type.parameterize(substituted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position-aware type resolution overload.
|
||||
* Resolves a type name with positional context so that an unqualified inner class name
|
||||
@@ -4330,7 +4378,7 @@ public class ScriptDocument {
|
||||
|
||||
// If already resolved, no need for positional fallback
|
||||
if (resolved != null && resolved.isResolved()) {
|
||||
return resolved;
|
||||
return substituteTypeParams(resolved, position);
|
||||
}
|
||||
|
||||
if (typeName != null && !typeName.contains(".")) {
|
||||
@@ -4343,14 +4391,7 @@ public class ScriptDocument {
|
||||
// Check if the name is a declared type parameter on this class (e.g., E in class Box<E>)
|
||||
TypeParamInfo typeParam = enclosing.getDeclaredTypeParam(baseTypeName);
|
||||
if (typeParam != null) {
|
||||
TypeInfo boundType = typeParam.getBoundTypeInfo();
|
||||
if (boundType == null) {
|
||||
typeParam.resolveBoundType();
|
||||
boundType = typeParam.getBoundTypeInfo();
|
||||
}
|
||||
TypeInfo result = (boundType != null && boundType.isResolved())
|
||||
? TypeInfo.typeParameter(baseTypeName, boundType)
|
||||
: TypeInfo.typeParameter(baseTypeName);
|
||||
TypeInfo result = TypeInfo.typeParameter(baseTypeName, typeParam);
|
||||
for (int i = 0; i < arrayDims; i++) result = TypeInfo.arrayOf(result);
|
||||
return result;
|
||||
}
|
||||
@@ -4586,7 +4627,7 @@ public class ScriptDocument {
|
||||
|
||||
// Modifiers - Java only
|
||||
if (!isJavaScript()) {
|
||||
addPatternMarks(marks, MODIFIER_PATTERN, TokenType.MODIFIER);
|
||||
addPatternMarks(marks, MODIFIER_PATTERN, TokenType.KEYWORD);
|
||||
}
|
||||
|
||||
// Type declarations and usages - Java only (JS doesn't have explicit types)
|
||||
@@ -5297,7 +5338,7 @@ public class ScriptDocument {
|
||||
continue;
|
||||
|
||||
// Mark 'import' keyword
|
||||
marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.IMPORT_KEYWORD, imp));
|
||||
marks.add(new ScriptLine.Mark(imp.getStartOffset(), imp.getStartOffset() + 6, TokenType.KEYWORD, imp));
|
||||
|
||||
// Parse path tokens
|
||||
int pathStart = imp.getPathStartOffset();
|
||||
@@ -5477,7 +5518,7 @@ public class ScriptDocument {
|
||||
if (isExcluded(m.start()))
|
||||
continue;
|
||||
|
||||
marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.CLASS_KEYWORD));
|
||||
marks.add(new ScriptLine.Mark(m.start(1), m.end(1), TokenType.KEYWORD));
|
||||
|
||||
String kind = m.group(1);
|
||||
TokenType nameType;
|
||||
@@ -5541,7 +5582,7 @@ public class ScriptDocument {
|
||||
int extIdx = indexOfAtDepthZero(betweenParamsAndBrace, "extends");
|
||||
if (extIdx >= 0) {
|
||||
int extendsAbsStart = scanPos + extIdx;
|
||||
marks.add(new ScriptLine.Mark(extendsAbsStart, extendsAbsStart + 7, TokenType.IMPORT_KEYWORD));
|
||||
marks.add(new ScriptLine.Mark(extendsAbsStart, extendsAbsStart + 7, TokenType.KEYWORD));
|
||||
|
||||
// Extract parent name after 'extends '
|
||||
String afterExtends = betweenParamsAndBrace.substring(extIdx + 7).trim();
|
||||
@@ -5574,7 +5615,7 @@ public class ScriptDocument {
|
||||
int implIdx = indexOfAtDepthZero(betweenParamsAndBrace, "implements");
|
||||
if (implIdx >= 0) {
|
||||
int implAbsStart = scanPos + implIdx;
|
||||
marks.add(new ScriptLine.Mark(implAbsStart, implAbsStart + 10, TokenType.IMPORT_KEYWORD));
|
||||
marks.add(new ScriptLine.Mark(implAbsStart, implAbsStart + 10, TokenType.KEYWORD));
|
||||
|
||||
String afterImpl = betweenParamsAndBrace.substring(implIdx + 10).trim();
|
||||
int implListStart = implAbsStart + 10;
|
||||
@@ -6426,7 +6467,7 @@ public class ScriptDocument {
|
||||
while (pos >= 0 && Character.isWhitespace(text.charAt(pos)))
|
||||
pos--;
|
||||
|
||||
if (pos < 0 || text.charAt(pos) != '.')
|
||||
if (pos < 0 || isExcluded(pos) || text.charAt(pos) != '.')
|
||||
return false;
|
||||
|
||||
// Skip the dot and any whitespace
|
||||
@@ -6434,7 +6475,7 @@ public class ScriptDocument {
|
||||
while (pos >= 0 && Character.isWhitespace(text.charAt(pos)))
|
||||
pos--;
|
||||
|
||||
if (pos < 0)
|
||||
if (pos < 0 || isExcluded(pos))
|
||||
return false;
|
||||
|
||||
// Check what's before the dot - could be:
|
||||
@@ -6465,7 +6506,7 @@ public class ScriptDocument {
|
||||
pos--;
|
||||
|
||||
// If preceded by a dot, this is part of a chain - treat as instance for now
|
||||
if (pos >= 0 && text.charAt(pos) == '.') {
|
||||
if (pos >= 0 && !isExcluded(pos) && text.charAt(pos) == '.') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7158,7 +7199,8 @@ public class ScriptDocument {
|
||||
while (scanPos >= 0 && Character.isWhitespace(text.charAt(scanPos)))
|
||||
scanPos--;
|
||||
|
||||
if (scanPos < 0 || text.charAt(scanPos) != '.') {
|
||||
// Guard: dot inside a comment/string must not be treated as a receiver chain separator
|
||||
if (scanPos < 0 || isExcluded(scanPos) || text.charAt(scanPos) != '.') {
|
||||
return null; // No receiver
|
||||
}
|
||||
|
||||
@@ -8089,11 +8131,24 @@ public class ScriptDocument {
|
||||
|
||||
if (Character.isJavaIdentifierPart(c)) {
|
||||
while (pos >= 0 && Character.isJavaIdentifierPart(text.charAt(pos))) pos--;
|
||||
// Skip whitespace to check for chained identifier (preceded by a dot)
|
||||
// Skip whitespace AND excluded ranges to check for chained identifier (preceded by a dot).
|
||||
// Without skipping excluded ranges, a dot inside a comment (e.g. "//event.\n player")
|
||||
// would be mistaken for a chain continuation dot.
|
||||
int checkPos = pos;
|
||||
while (checkPos >= 0 && Character.isWhitespace(text.charAt(checkPos))) checkPos--;
|
||||
// If it's part of a chained identifier (preceded by a dot), continue
|
||||
if (checkPos >= 0 && text.charAt(checkPos) == '.') { pos = checkPos - 1; continue; }
|
||||
while (checkPos >= 0) {
|
||||
if (isExcluded(checkPos)) {
|
||||
for (int[] range : excludedRanges) {
|
||||
if (checkPos >= range[0] && checkPos < range[1]) {
|
||||
checkPos = range[0] - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (Character.isWhitespace(text.charAt(checkPos))) { checkPos--; continue; }
|
||||
break;
|
||||
}
|
||||
if (checkPos >= 0 && !isExcluded(checkPos) && text.charAt(checkPos) == '.') { pos = checkPos - 1; continue; }
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -8201,7 +8256,7 @@ for (ScriptTypeInfo type:scriptTypes.values()) {
|
||||
superClass, parentConstructor, false).setConstructor(true);
|
||||
callInfo.validate();
|
||||
methodCalls.add(callInfo);
|
||||
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.IMPORT_KEYWORD,
|
||||
marks.add(new ScriptLine.Mark(nameStart, nameEnd, TokenType.KEYWORD,
|
||||
errorMsg != null ? errorMsg : callInfo));
|
||||
}
|
||||
|
||||
@@ -9974,7 +10029,7 @@ for (ScriptTypeInfo type:scriptTypes.values()) {
|
||||
|
||||
|
||||
TokenType type = isClassTypeInfo ? varInfo.isGlobal() ? TokenType.GLOBAL_FIELD
|
||||
: TokenType.LOCAL_FIELD : info.getTokenType();
|
||||
: TokenType.LOCAL_FIELD : TokenType.METHOD_CALL;
|
||||
marks.add(new ScriptLine.Mark(start, end, type, isClassTypeInfo ? varInfo : ctorCall));
|
||||
continue;
|
||||
}
|
||||
@@ -10218,7 +10273,7 @@ for (ScriptTypeInfo type:scriptTypes.values()) {
|
||||
int i = position - 1;
|
||||
while (i >= 0 && Character.isWhitespace(text.charAt(i)))
|
||||
i--;
|
||||
return i >= 0 && text.charAt(i) == '.';
|
||||
return i >= 0 && text.charAt(i) == '.' && !isExcluded(i);
|
||||
}
|
||||
|
||||
boolean isInImportOrPackage(int position) {
|
||||
|
||||
@@ -132,15 +132,22 @@ public class ScriptLine {
|
||||
clearTokens();
|
||||
int cursor = globalStart;
|
||||
|
||||
/*
|
||||
* globalEnd includes the newline character position (lineEnd = lineStart + text.length() + 1),
|
||||
* but the line's text field does NOT include the newline. We must clamp all token boundaries
|
||||
* to the displayable text end so that '\n' never leaks into rendered token text.
|
||||
*/
|
||||
int displayEnd = Math.min(globalStart + text.length(), fullText.length());
|
||||
|
||||
for (Mark mark : marks) {
|
||||
// Skip marks that don't overlap this line
|
||||
if (mark.end <= globalStart || mark.start >= globalEnd) {
|
||||
// Skip marks that don't overlap the displayable portion of this line
|
||||
if (mark.end <= globalStart || mark.start >= displayEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clamp mark to line boundaries
|
||||
// Clamp mark to displayable line boundaries (excludes trailing newline)
|
||||
int tokenStart = Math.max(mark.start, globalStart);
|
||||
int tokenEnd = Math.min(mark.end, globalEnd);
|
||||
int tokenEnd = Math.min(mark.end, displayEnd);
|
||||
|
||||
// Validate bounds
|
||||
tokenStart = Math.max(0, Math.min(tokenStart, fullText.length()));
|
||||
@@ -168,13 +175,10 @@ public class ScriptLine {
|
||||
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));
|
||||
}
|
||||
// Add trailing default token if needed (clamped to displayable text, excluding newline)
|
||||
if (cursor < displayEnd) {
|
||||
String trailingText = fullText.substring(cursor, displayEnd);
|
||||
addToken(Token.defaultToken(trailingText, cursor, displayEnd));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,12 +187,7 @@ public class ScriptLine {
|
||||
token.setTypeInfo((TypeInfo) metadata);
|
||||
} else if (metadata instanceof TypeParamInfo) {
|
||||
TypeParamInfo typeParam = (TypeParamInfo) metadata;
|
||||
TypeInfo boundType = typeParam.getBoundTypeInfo();
|
||||
if (boundType != null) {
|
||||
token.setTypeInfo(TypeInfo.typeParameter(typeParam.getName(), boundType));
|
||||
} else {
|
||||
token.setTypeInfo(TypeInfo.typeParameter(typeParam.getName()));
|
||||
}
|
||||
token.setTypeInfo(TypeInfo.typeParameter(typeParam.getName(), typeParam));
|
||||
} else if (metadata instanceof MethodCallInfo) {
|
||||
MethodCallInfo callInfo = (MethodCallInfo) metadata;
|
||||
if (callInfo.isConstructor()) {
|
||||
@@ -234,6 +233,59 @@ public class ScriptLine {
|
||||
|
||||
// ==================== RENDERING ====================
|
||||
|
||||
/**
|
||||
* Compute the rendered pixel width of a substring of this line, accounting for
|
||||
* bold/italic token styles. Characters covered by bold or italic tokens will be
|
||||
* measured with the appropriate font style, producing the same widths as the
|
||||
* actual rendered output.
|
||||
*
|
||||
* @param localStart start index within the line text (inclusive)
|
||||
* @param localEnd end index within the line text (exclusive)
|
||||
* @return pixel width matching the rendered output
|
||||
*/
|
||||
public int getRenderedWidth(int localStart, int localEnd) {
|
||||
localStart = Math.max(0, Math.min(localStart, text.length()));
|
||||
localEnd = Math.max(localStart, Math.min(localEnd, text.length()));
|
||||
if (localStart >= localEnd) return 0;
|
||||
|
||||
int width = 0;
|
||||
int cursor = localStart;
|
||||
|
||||
for (Token t : tokens) {
|
||||
if (cursor >= localEnd) break;
|
||||
|
||||
int tokenLocalStart = t.getGlobalStart() - globalStart;
|
||||
int tokenLocalEnd = tokenLocalStart + t.getText().length();
|
||||
|
||||
// Gap before this token (rendered as plain text)
|
||||
if (cursor < tokenLocalStart) {
|
||||
int gapEnd = Math.min(tokenLocalStart, localEnd);
|
||||
if (gapEnd > cursor) {
|
||||
width += ClientProxy.Font.width(text.substring(cursor, gapEnd));
|
||||
cursor = gapEnd;
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor >= localEnd) break;
|
||||
|
||||
// Token overlap with [localStart, localEnd)
|
||||
int overlapStart = Math.max(cursor, tokenLocalStart);
|
||||
int overlapEnd = Math.min(tokenLocalEnd, localEnd);
|
||||
if (overlapEnd > overlapStart) {
|
||||
String substr = text.substring(overlapStart, overlapEnd);
|
||||
width += ClientProxy.Font.width(substr, t.getFontStyle());
|
||||
cursor = overlapEnd;
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing text after last token (plain)
|
||||
if (cursor < localEnd) {
|
||||
width += ClientProxy.Font.width(text.substring(cursor, localEnd));
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw this line with syntax highlighting using Minecraft color codes.
|
||||
* Compatible with the existing rendering system.
|
||||
@@ -249,7 +301,7 @@ public class ScriptLine {
|
||||
|
||||
for (Token t : tokens) {
|
||||
int tokenStart = t.getGlobalStart() - globalStart; // relative position in line
|
||||
int tokenWidth = ClientProxy.Font.width(t.getText());
|
||||
int tokenWidth = ClientProxy.Font.width(t.getText(), t.getFontStyle());
|
||||
|
||||
// Calculate gap width before this token
|
||||
if (tokenStart > lastIndex && tokenStart <= text.length()) {
|
||||
@@ -293,12 +345,9 @@ public class ScriptLine {
|
||||
* 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
|
||||
|
||||
// Build segments with token position info and compute positions using style-aware widths
|
||||
java.util.List<TextSegment> segments = new java.util.ArrayList<>();
|
||||
|
||||
for (Token t : tokens) {
|
||||
@@ -307,37 +356,29 @@ public class ScriptLine {
|
||||
// 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);
|
||||
segments.add(new TextSegment(lastIndex, gap, 0xFFFFFFFF, false));
|
||||
}
|
||||
|
||||
// Add the colored token
|
||||
// Add the colored token (with style prefix for rendering)
|
||||
String styledText = t.getStylePrefix() + t.getText();
|
||||
segments.add(new TextSegment(fullText.length(), styledText, t.getHexColor(), true));
|
||||
fullText.append(styledText);
|
||||
|
||||
lastIndex = tokenStart + styledText.length();
|
||||
segments.add(new TextSegment(tokenStart, styledText, t.getHexColor(), true));
|
||||
|
||||
lastIndex = tokenStart + t.getText().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);
|
||||
segments.add(new TextSegment(lastIndex, remaining, 0xFFFFFFFF, false));
|
||||
}
|
||||
|
||||
// Draw each segment at the correct position
|
||||
// Calculate positions based on the full string to match drawString's spacing
|
||||
// Draw each segment at the correct position using style-aware width
|
||||
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;
|
||||
int prefixWidth = getRenderedWidth(0, seg.startPos);
|
||||
int 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +395,7 @@ public class ScriptLine {
|
||||
|
||||
ErrorUnderlineRenderer.drawErrorUnderlines(
|
||||
parent,
|
||||
this,
|
||||
lineStartX,
|
||||
baselineY,
|
||||
getText(),
|
||||
|
||||
@@ -210,10 +210,7 @@ public class ScriptTextContainer extends JavaTextContainer {
|
||||
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;
|
||||
|
||||
@@ -9,7 +9,7 @@ import noppes.npcs.client.gui.util.script.interpreter.jsdoc.JSDocInfo;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.token.TokenType;
|
||||
import noppes.npcs.client.gui.util.script.interpreter.type.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;
|
||||
|
||||
@@ -18,7 +18,6 @@ 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.
|
||||
@@ -165,12 +164,8 @@ public final class FieldInfo {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
if (GenericContext.hasGenerics(containingType))
|
||||
type = GenericContext.forReceiver(containingType).substitute(type);
|
||||
|
||||
// Check if this is an enum constant
|
||||
if (field.isEnumConstant()) {
|
||||
@@ -531,6 +526,38 @@ public final class FieldInfo {
|
||||
public String getDocumentation() { return documentation; }
|
||||
public JSDocInfo getJSDocInfo() { return jsDocInfo; }
|
||||
public void setJSDocInfo(JSDocInfo jsDocInfo) { this.jsDocInfo = jsDocInfo; }
|
||||
|
||||
/**
|
||||
* Create a copy of this field with its declared type substituted using the given context.
|
||||
* Returns {@code this} unchanged if the context is null, the declared type is null,
|
||||
* or substitution produces the same type (identity check).
|
||||
*
|
||||
* Used when a generic type is parameterized (e.g., {@code Box<String>}) and its raw fields
|
||||
* (declared with type variables like T) need to reflect the concrete type arguments.
|
||||
*
|
||||
* Example: for {@code class Box<T> { T value; }}, calling this on the raw field {@code value: T}
|
||||
* with a context binding {@code T → String} produces {@code value: String}.
|
||||
*
|
||||
* @param context the generic context containing type variable bindings; if null, returns {@code this} unchanged
|
||||
* @return a new FieldInfo with the substituted declared type, or {@code this} if no substitution occurred
|
||||
*/
|
||||
public FieldInfo substituteTypeParams(GenericContext context) {
|
||||
if (context == null || declaredType == null) {
|
||||
return this;
|
||||
}
|
||||
TypeInfo subType = context.substitute(declaredType);
|
||||
if (subType == declaredType) {
|
||||
return this;
|
||||
}
|
||||
FieldInfo result = new FieldInfo(name, scope, subType, declarationOffset, resolved,
|
||||
containingMethod, documentation, initStart, initEnd, modifiers, reflectionField);
|
||||
result.jsDocInfo = this.jsDocInfo;
|
||||
result.scopeInfo = this.scopeInfo;
|
||||
result.isVarArg = this.isVarArg;
|
||||
result.inferredType = this.inferredType;
|
||||
return result;
|
||||
}
|
||||
|
||||
public int getInitStart() { return initStart; }
|
||||
public int getInitEnd() { return initEnd; }
|
||||
public boolean hasInitializer() { return initStart >= 0 && initEnd > initStart; }
|
||||
|
||||
+1
-1
@@ -282,7 +282,7 @@ public class GutterIconRenderer {
|
||||
int currentY = y + PADDING;
|
||||
|
||||
for (TextSegment segment : segments) {
|
||||
ClientProxy.Font.drawString(segment.text, currentX, currentY, 0xFF000000 | segment.color);
|
||||
ClientProxy.Font.drawString(segment.text, currentX, currentY, segment.color);
|
||||
currentX += ClientProxy.Font.width(segment.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import noppes.npcs.client.gui.util.script.interpreter.token.Token;
|
||||
public class HoverState {
|
||||
|
||||
/** Minimum hover time (ms) before showing tooltip */
|
||||
private static final long HOVER_DELAY_MS = 100;
|
||||
private static final long HOVER_DELAY_MS = 250;
|
||||
|
||||
// ==================== STATE ====================
|
||||
|
||||
@@ -89,6 +89,11 @@ public class HoverState {
|
||||
/** Whether click-to-pin behaviour is enabled for this hover state. */
|
||||
private boolean clickToPinEnabled = true;
|
||||
|
||||
/** Locked mouse position for tooltip placement (captured once when tooltip becomes visible) */
|
||||
private int lockedTooltipMouseX;
|
||||
private int lockedTooltipMouseY;
|
||||
private boolean isTooltipPositionLocked;
|
||||
|
||||
// ==================== UPDATE ====================
|
||||
|
||||
/**
|
||||
@@ -172,6 +177,9 @@ public class HoverState {
|
||||
if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) {
|
||||
tooltipVisible = true;
|
||||
hoverInfo = TokenHoverInfo.fromToken(token);
|
||||
lockedTooltipMouseX = lastMouseX;
|
||||
lockedTooltipMouseY = lastMouseY;
|
||||
isTooltipPositionLocked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,6 +231,9 @@ public class HoverState {
|
||||
if (elapsed >= HOVER_DELAY_MS && !tooltipVisible) {
|
||||
tooltipVisible = true;
|
||||
hoverInfo = TokenHoverInfo.fromToken(token);
|
||||
lockedTooltipMouseX = lastMouseX;
|
||||
lockedTooltipMouseY = lastMouseY;
|
||||
isTooltipPositionLocked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,6 +260,7 @@ public class HoverState {
|
||||
targetScrollOffset = 0;
|
||||
tooltipMaxScroll = 0;
|
||||
isDraggingScrollbar = false;
|
||||
isTooltipPositionLocked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,6 +296,11 @@ public class HoverState {
|
||||
this.tokenWidth = tokenW;
|
||||
// ensure hoveredToken reflects pinned token
|
||||
this.hoveredToken = token;
|
||||
if (this.tooltipVisible && !this.isTooltipPositionLocked) {
|
||||
this.lockedTooltipMouseX = lastMouseX;
|
||||
this.lockedTooltipMouseY = lastMouseY;
|
||||
this.isTooltipPositionLocked = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,14 +311,20 @@ public class HoverState {
|
||||
this.pinnedHoverInfo = null;
|
||||
this.tooltipVisible = false;
|
||||
this.hoverInfo = null;
|
||||
this.isTooltipPositionLocked = false;
|
||||
}
|
||||
|
||||
public boolean isPinned() { return pinnedToken != null; }
|
||||
|
||||
public int getLockedMouseX() { return lockedTooltipMouseX; }
|
||||
public int getLockedMouseY() { return lockedTooltipMouseY; }
|
||||
public boolean isPositionLocked() { return isTooltipPositionLocked; }
|
||||
|
||||
// ==================== POSITION CALCULATION ====================
|
||||
|
||||
/**
|
||||
* Get the character index within a line at the given X pixel position.
|
||||
* Uses style-aware width calculation to account for bold/italic tokens.
|
||||
*/
|
||||
private int getCharacterIndexAtX(ScriptLine line, int x) {
|
||||
String text = line.getText();
|
||||
@@ -309,7 +332,7 @@ public class HoverState {
|
||||
|
||||
int accumWidth = 0;
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
int charWidth = noppes.npcs.client.ClientProxy.Font.width(String.valueOf(text.charAt(i)));
|
||||
int charWidth = line.getRenderedWidth(i, i + 1);
|
||||
if (accumWidth + charWidth / 2 > x) {
|
||||
return i;
|
||||
}
|
||||
@@ -324,20 +347,19 @@ public class HoverState {
|
||||
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();
|
||||
// X position: calculate width of text before the token (style-aware)
|
||||
int tokenLocalStart = token.getGlobalStart() - line.getGlobalStart();
|
||||
tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, lineText.length()));
|
||||
tokenLocalStart = Math.max(0, Math.min(tokenLocalStart, line.getText().length()));
|
||||
|
||||
String textBefore = lineText.substring(0, tokenLocalStart);
|
||||
tokenScreenX = viewportX + noppes.npcs.client.ClientProxy.Font.width(textBefore);
|
||||
tokenScreenX = viewportX + line.getRenderedWidth(0, tokenLocalStart);
|
||||
|
||||
// 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());
|
||||
// Token width (style-aware)
|
||||
int tokenLocalEnd = tokenLocalStart + token.getText().length();
|
||||
tokenWidth = line.getRenderedWidth(tokenLocalStart, tokenLocalEnd);
|
||||
}
|
||||
|
||||
public void setTooltipBounds(int x, int y, int width, int height) {
|
||||
|
||||
+104
-46
@@ -185,7 +185,6 @@ public class TokenHoverInfo {
|
||||
break;
|
||||
case LITERAL:
|
||||
case KEYWORD:
|
||||
case MODIFIER:
|
||||
case STRING:
|
||||
case COMMENT:
|
||||
if (info.hasErrors())
|
||||
@@ -266,17 +265,17 @@ public class TokenHoverInfo {
|
||||
int mods = clazz.getModifiers();
|
||||
|
||||
// Modifiers
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isAbstract(mods) && !clazz.isInterface()) addSegment("abstract ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isFinal(mods) && !isEnum) addSegment("final ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isAbstract(mods) && !clazz.isInterface()) addSegment("abstract ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isFinal(mods) && !isEnum) addSegment("final ", TokenType.KEYWORD.getHexColor());
|
||||
|
||||
// Class type keyword
|
||||
if (clazz.isInterface()) {
|
||||
addSegment("interface ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("interface ", TokenType.KEYWORD.getHexColor());
|
||||
} else if (isEnum) {
|
||||
addSegment("enum ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("enum ", TokenType.KEYWORD.getHexColor());
|
||||
} else {
|
||||
addSegment("class ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("class ", TokenType.KEYWORD.getHexColor());
|
||||
}
|
||||
|
||||
// Class name - use proper color based on type
|
||||
@@ -290,7 +289,7 @@ public class TokenHoverInfo {
|
||||
// Extends
|
||||
Class<?> superclass = clazz.getSuperclass();
|
||||
if (superclass != null && superclass != Object.class && !isEnum) {
|
||||
addSegment(" extends ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment(" extends ", TokenType.KEYWORD.getHexColor());
|
||||
addSegment(superclass.getSimpleName(), getColorForClass(superclass));
|
||||
}
|
||||
|
||||
@@ -299,7 +298,7 @@ public class TokenHoverInfo {
|
||||
// Implements
|
||||
Class<?>[] interfaces = clazz.getInterfaces();
|
||||
if (interfaces.length > 0) {
|
||||
addSegment(clazz.isInterface() ? " extends " : " implements ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment(clazz.isInterface() ? " extends " : " implements ", TokenType.KEYWORD.getHexColor());
|
||||
for (int i = 0; i < Math.min(interfaces.length, 3); i++) {
|
||||
if (i > 0) addSegment(", ", TokenType.DEFAULT.getHexColor());
|
||||
addSegment(interfaces[i].getSimpleName(), TokenType.INTERFACE_DECL.getHexColor());
|
||||
@@ -325,19 +324,19 @@ public class TokenHoverInfo {
|
||||
int mods = scriptType.getModifiers();
|
||||
|
||||
// Modifiers
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isAbstract(mods) && scriptType.getKind() != TypeInfo.Kind.INTERFACE)
|
||||
addSegment("abstract ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isFinal(mods)) addSegment("final ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isStatic(mods)) addSegment("static ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("abstract ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isFinal(mods)) addSegment("final ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isStatic(mods)) addSegment("static ", TokenType.KEYWORD.getHexColor());
|
||||
|
||||
// Class type keyword
|
||||
if (scriptType.getKind() == TypeInfo.Kind.INTERFACE) {
|
||||
addSegment("interface ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("interface ", TokenType.KEYWORD.getHexColor());
|
||||
} else if (scriptType.getKind() == TypeInfo.Kind.ENUM) {
|
||||
addSegment("enum ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("enum ", TokenType.KEYWORD.getHexColor());
|
||||
} else {
|
||||
addSegment("class ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("class ", TokenType.KEYWORD.getHexColor());
|
||||
}
|
||||
|
||||
// Class name
|
||||
@@ -347,7 +346,7 @@ public class TokenHoverInfo {
|
||||
addTypeSegments(typeInfo);
|
||||
addDeclaredTypeParamSegments(typeInfo);
|
||||
if (scriptType.hasSuperClass()) {
|
||||
addSegment(" extends ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment(" extends ", TokenType.KEYWORD.getHexColor());
|
||||
TypeInfo superClass = scriptType.getSuperClass();
|
||||
if (superClass != null && superClass.isResolved()) {
|
||||
// Color based on resolved type
|
||||
@@ -370,7 +369,7 @@ public class TokenHoverInfo {
|
||||
// Already showed extends, so use comma
|
||||
addSegment(", ", TokenType.DEFAULT.getHexColor());
|
||||
} else {
|
||||
addSegment(keyword, TokenType.MODIFIER.getHexColor());
|
||||
addSegment(keyword, TokenType.KEYWORD.getHexColor());
|
||||
}
|
||||
|
||||
List<String> interfaceNames = scriptType.getImplementedInterfaceNames();
|
||||
@@ -414,12 +413,12 @@ public class TokenHoverInfo {
|
||||
JSTypeInfo jsType = typeInfo.getJSTypeInfo();
|
||||
|
||||
iconIndicator = "I";
|
||||
addSegment("interface ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("interface ", TokenType.KEYWORD.getHexColor());
|
||||
addTypeSegments(typeInfo);
|
||||
addDeclaredTypeParamSegments(typeInfo);
|
||||
|
||||
if (jsType.getExtendsType() != null) {
|
||||
addSegment(" extends ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment(" extends ", TokenType.KEYWORD.getHexColor());
|
||||
addSegment(jsType.getExtendsType(), TokenType.INTERFACE_DECL.getHexColor());
|
||||
}
|
||||
|
||||
@@ -442,7 +441,7 @@ public class TokenHoverInfo {
|
||||
if (token.getMethodInfo() != null) {
|
||||
MethodInfo constructor = token.getMethodInfo();
|
||||
declaration.add(new TextSegment("\n", TokenType.DEFAULT.getHexColor()));
|
||||
additionalInfo.add("Constructor");
|
||||
// additionalInfo.add("Constructor");
|
||||
buildConstructorDeclaration(constructor, typeInfo);
|
||||
}
|
||||
}
|
||||
@@ -451,13 +450,13 @@ public class TokenHoverInfo {
|
||||
iconIndicator = "T";
|
||||
String paramName = typeInfo.getTypeParameterName();
|
||||
|
||||
addSegment("type parameter ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("type parameter ", TokenType.KEYWORD.getHexColor());
|
||||
addSegment("<", TokenType.DEFAULT.getHexColor());
|
||||
addSegment(paramName, TokenType.GENERIC_TYPE_PARAM.getHexColor());
|
||||
|
||||
TypeInfo bound = typeInfo.getBoundType();
|
||||
if (bound != null && bound.getJavaClass() != null && bound.getJavaClass() != Object.class) {
|
||||
addSegment(" extends ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment(" extends ", TokenType.KEYWORD.getHexColor());
|
||||
addSegment(bound.getSimpleName(), getColorForTypeInfo(bound));
|
||||
}
|
||||
addSegment(">", TokenType.DEFAULT.getHexColor());
|
||||
@@ -583,7 +582,7 @@ public class TokenHoverInfo {
|
||||
// // Show modifiers from source if we have a declaration position
|
||||
// String modifiers = extractModifiersAtPosition(fieldInfo.getDeclarationOffset());
|
||||
// if (modifiers != null && !modifiers.isEmpty()) {
|
||||
// addSegment(modifiers + " ", TokenType.MODIFIER.getHexColor());
|
||||
// addSegment(modifiers + " ", TokenType.KEYWORD.getHexColor());
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -673,7 +672,7 @@ public class TokenHoverInfo {
|
||||
addInitializationTokens(token, fieldInfo);
|
||||
|
||||
// Show it's a local variable
|
||||
additionalInfo.add("Local variable");
|
||||
// additionalInfo.add("Local variable");
|
||||
}
|
||||
|
||||
public String getPackageName(TypeInfo type) {
|
||||
@@ -722,7 +721,7 @@ public class TokenHoverInfo {
|
||||
|
||||
addSegment(fieldInfo.getName(), TokenType.PARAMETER.getHexColor());
|
||||
|
||||
additionalInfo.add("Parameter");
|
||||
// additionalInfo.add("Parameter");
|
||||
}
|
||||
|
||||
private void extractUndefinedInfo(Token token) {
|
||||
@@ -796,13 +795,72 @@ public class TokenHoverInfo {
|
||||
TypeParamInfo param = params.get(i);
|
||||
addSegment(param.getName(), TokenType.GENERIC_TYPE_PARAM.getHexColor());
|
||||
if (param.getBoundTypeName() != null && !"Object".equals(param.getBoundTypeName())) {
|
||||
addSegment(" extends ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment(" extends ", TokenType.KEYWORD.getHexColor());
|
||||
TypeInfo boundType = param.getBoundTypeInfo();
|
||||
if (boundType != null && boundType.isResolved()) {
|
||||
addTypeSegments(boundType);
|
||||
} else {
|
||||
addSegment(param.getBoundTypeName(), TokenType.IMPORTED_CLASS.getHexColor());
|
||||
}
|
||||
|
||||
List<TypeInfo> additionalBoundTypes = param.getAdditionalBoundTypes();
|
||||
List<String> additionalBoundNames = param.getAdditionalBoundNames();
|
||||
for (int j = 0; j < additionalBoundNames.size(); j++) {
|
||||
addSegment(" & ", TokenType.DEFAULT.getHexColor());
|
||||
TypeInfo additionalBoundType = j < additionalBoundTypes.size() ? additionalBoundTypes.get(j) : null;
|
||||
if (additionalBoundType != null && additionalBoundType.isResolved()) {
|
||||
addTypeSegments(additionalBoundType);
|
||||
if (!additionalBoundType.isParameterized()) {
|
||||
addBoundTypeParamSegments(additionalBoundType);
|
||||
}
|
||||
} else {
|
||||
addSegment(additionalBoundNames.get(j), TokenType.IMPORTED_CLASS.getHexColor());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addSegment(">", TokenType.DEFAULT.getHexColor());
|
||||
}
|
||||
|
||||
private void addBoundTypeParamSegments(TypeInfo boundType) {
|
||||
List<TypeParamInfo> params = boundType.getTypeParams();
|
||||
if (params == null || params.isEmpty()) return;
|
||||
|
||||
addSegment("<", TokenType.DEFAULT.getHexColor());
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
if (i > 0)
|
||||
addSegment(", ", TokenType.DEFAULT.getHexColor());
|
||||
TypeParamInfo tp = params.get(i);
|
||||
addSegment(tp.getName(), TokenType.GENERIC_TYPE_PARAM.getHexColor());
|
||||
|
||||
if (tp.getBoundTypeName() != null && !"Object".equals(tp.getBoundTypeName())) {
|
||||
addSegment(" extends ", TokenType.KEYWORD.getHexColor());
|
||||
TypeInfo innerBound = tp.getBoundTypeInfo();
|
||||
if (innerBound != null && innerBound.isResolved()) {
|
||||
addTypeSegments(innerBound);
|
||||
if (!innerBound.isParameterized()) {
|
||||
addBoundTypeParamSegments(innerBound);
|
||||
}
|
||||
} else {
|
||||
addSegment(tp.getBoundTypeName(), TokenType.IMPORTED_CLASS.getHexColor());
|
||||
}
|
||||
|
||||
List<TypeInfo> innerAdditional = tp.getAdditionalBoundTypes();
|
||||
List<String> innerAdditionalNames = tp.getAdditionalBoundNames();
|
||||
for (int j = 0; j < innerAdditionalNames.size(); j++) {
|
||||
addSegment(" & ", TokenType.DEFAULT.getHexColor());
|
||||
TypeInfo innerAddBound = j < innerAdditional.size() ? innerAdditional.get(j) : null;
|
||||
if (innerAddBound != null && innerAddBound.isResolved()) {
|
||||
addTypeSegments(innerAddBound);
|
||||
if (!innerAddBound.isParameterized()) {
|
||||
addBoundTypeParamSegments(innerAddBound);
|
||||
}
|
||||
} else {
|
||||
addSegment(innerAdditionalNames.get(j), TokenType.IMPORTED_CLASS.getHexColor());
|
||||
}
|
||||
}
|
||||
addSegment(" extends ", TokenType.KEYWORD.getHexColor());
|
||||
}
|
||||
}
|
||||
addSegment(">", TokenType.DEFAULT.getHexColor());
|
||||
@@ -844,9 +902,9 @@ public class TokenHoverInfo {
|
||||
// Modifiers (public, private, etc.)
|
||||
declaration.clear();
|
||||
int mods = constructor.getModifiers();
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor());
|
||||
else if (Modifier.isProtected(mods)) addSegment("protected ", TokenType.MODIFIER.getHexColor());
|
||||
else if (Modifier.isPrivate(mods)) addSegment("private ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.KEYWORD.getHexColor());
|
||||
else if (Modifier.isProtected(mods)) addSegment("protected ", TokenType.KEYWORD.getHexColor());
|
||||
else if (Modifier.isPrivate(mods)) addSegment("private ", TokenType.KEYWORD.getHexColor());
|
||||
|
||||
// Constructor name (same as class name)
|
||||
addSegment(constructor.getName(), containingType.getTokenType().getHexColor());
|
||||
@@ -890,14 +948,14 @@ public class TokenHoverInfo {
|
||||
// Skip for now - could add later
|
||||
|
||||
// Modifiers
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.MODIFIER.getHexColor());
|
||||
else if (Modifier.isProtected(mods)) addSegment("protected ", TokenType.MODIFIER.getHexColor());
|
||||
else if (Modifier.isPrivate(mods)) addSegment("private ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isPublic(mods)) addSegment("public ", TokenType.KEYWORD.getHexColor());
|
||||
else if (Modifier.isProtected(mods)) addSegment("protected ", TokenType.KEYWORD.getHexColor());
|
||||
else if (Modifier.isPrivate(mods)) addSegment("private ", TokenType.KEYWORD.getHexColor());
|
||||
|
||||
if (Modifier.isStatic(mods)) addSegment("static ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isFinal(mods)) addSegment("final ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isAbstract(mods)) addSegment("abstract ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isSynchronized(mods)) addSegment("synchronized ", TokenType.MODIFIER.getHexColor());
|
||||
if (Modifier.isStatic(mods)) addSegment("static ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isFinal(mods)) addSegment("final ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isAbstract(mods)) addSegment("abstract ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isSynchronized(mods)) addSegment("synchronized ", TokenType.KEYWORD.getHexColor());
|
||||
|
||||
// Return type - check for actual type color (handle array suffix coloring)
|
||||
TypeInfo returnType = methodInfo.getReturnType();
|
||||
@@ -932,12 +990,12 @@ public class TokenHoverInfo {
|
||||
if (methodInfo.isDeclaration() && methodInfo.getDeclarationOffset() >= 0) {
|
||||
String modifiers = extractModifiersAtPosition(methodInfo.getDeclarationOffset());
|
||||
if (modifiers != null && !modifiers.isEmpty()) {
|
||||
addSegment(modifiers + " ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment(modifiers + " ", TokenType.KEYWORD.getHexColor());
|
||||
}
|
||||
} else {
|
||||
// Fallback: show static if we know it
|
||||
if (methodInfo.isStatic()) {
|
||||
addSegment("static ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("static ", TokenType.KEYWORD.getHexColor());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1395,20 +1453,20 @@ public class TokenHoverInfo {
|
||||
*/
|
||||
private void addFieldModifiers(int mods) {
|
||||
if (Modifier.isPublic(mods))
|
||||
addSegment("public ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("public ", TokenType.KEYWORD.getHexColor());
|
||||
else if (Modifier.isProtected(mods))
|
||||
addSegment("protected ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("protected ", TokenType.KEYWORD.getHexColor());
|
||||
else if (Modifier.isPrivate(mods))
|
||||
addSegment("private ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("private ", TokenType.KEYWORD.getHexColor());
|
||||
|
||||
if (Modifier.isStatic(mods))
|
||||
addSegment("static ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("static ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isFinal(mods))
|
||||
addSegment("final ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("final ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isVolatile(mods))
|
||||
addSegment("volatile ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("volatile ", TokenType.KEYWORD.getHexColor());
|
||||
if (Modifier.isTransient(mods))
|
||||
addSegment("transient ", TokenType.MODIFIER.getHexColor());
|
||||
addSegment("transient ", TokenType.KEYWORD.getHexColor());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+29
-11
@@ -39,7 +39,7 @@ public class TokenHoverRenderer {
|
||||
private static final int SEPARATOR_SPACING = 5;
|
||||
|
||||
/** Vertical offset from token */
|
||||
private static final int VERTICAL_OFFSET = 4;
|
||||
private static final int VERTICAL_OFFSET = 1;
|
||||
|
||||
/** Maximum tooltip width as percentage of viewport */
|
||||
private static final float MAX_WIDTH_RATIO = 0.9f;
|
||||
@@ -137,9 +137,20 @@ public class TokenHoverRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Position the tooltip — use dragged position if user has panned the panel,
|
||||
// otherwise use locked mouse position (captured when tooltip first became visible)
|
||||
int tooltipX;
|
||||
int tooltipY;
|
||||
if (positionOverridden) {
|
||||
tooltipX = hoverState.getOverriddenTooltipX();
|
||||
tooltipY = hoverState.getOverriddenTooltipY();
|
||||
} else if (hoverState.isPositionLocked()) {
|
||||
tooltipX = hoverState.getLockedMouseX() + 2;
|
||||
tooltipY = tokenY + lineHeight + VERTICAL_OFFSET;
|
||||
} else {
|
||||
tooltipX = tokenX;
|
||||
tooltipY = tokenY + lineHeight + VERTICAL_OFFSET;
|
||||
}
|
||||
|
||||
// Clamp position — use screen bounds when user has dragged the panel outside the editor viewport
|
||||
int leftBound = useScreenBounds ? 0 : viewportX;
|
||||
@@ -164,7 +175,7 @@ public class TokenHoverRenderer {
|
||||
if (useScreenBounds) {
|
||||
tooltipY = bottomBound - boxHeight;
|
||||
} else {
|
||||
tooltipY = tokenY - boxHeight - VERTICAL_OFFSET; // try above token
|
||||
tooltipY = tokenY - boxHeight;
|
||||
}
|
||||
}
|
||||
if (tooltipY < topBound) {
|
||||
@@ -176,13 +187,20 @@ public class TokenHoverRenderer {
|
||||
|
||||
// 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
|
||||
// When default and tooltip is BELOW the token, union the token bottom and the tooltip rect
|
||||
// so the small gap between token and tooltip doesn't break hover detection.
|
||||
// When tooltip is ABOVE the token, use only the tooltip rect — bridging would cover
|
||||
// intervening tokens and prevent hover-switching between them.
|
||||
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);
|
||||
} else if (tooltipY >= tokenY + lineHeight) {
|
||||
// Tooltip is below the token — bridge the gap
|
||||
int boundsTopY = tokenY + lineHeight;
|
||||
int boundsBottomY = tooltipY + boxHeight;
|
||||
hoverState.setTooltipBounds(tooltipX, boundsTopY, boxWidth, boundsBottomY - boundsTopY);
|
||||
} else {
|
||||
// Tooltip is above the token — use tooltip rect only
|
||||
hoverState.setTooltipBounds(tooltipX, tooltipY, boxWidth, boxHeight);
|
||||
}
|
||||
|
||||
// Render the tooltip
|
||||
@@ -218,7 +236,7 @@ public class TokenHoverRenderer {
|
||||
int boxHeight = visibleContentHeight + PADDING * 2;
|
||||
|
||||
// Store scroll metadata into HoverState
|
||||
hoverState.setTooltipMaxScroll(Math.max(0, totalContentHeight - visibleContentHeight + BOTTOM_GAP));
|
||||
hoverState.setTooltipMaxScroll(Math.max(0, totalContentHeight - visibleContentHeight + (hasScrollbar? BOTTOM_GAP : 0)));
|
||||
|
||||
// Draw background
|
||||
Gui.drawRect(x, y, x + boxWidth, y + boxHeight, BG_COLOR);
|
||||
@@ -437,7 +455,7 @@ public class TokenHoverRenderer {
|
||||
|
||||
for (TokenHoverInfo.TextSegment segment : segments) {
|
||||
String text = segment.text;
|
||||
int color = 0xFF000000 | segment.color;
|
||||
int color = segment.color;
|
||||
|
||||
// Split segment into words for wrapping
|
||||
String[] words = text.split("(?<=\\s)|(?=\\s)"); // Keep whitespace as separate tokens
|
||||
|
||||
+38
-9
@@ -11,7 +11,7 @@ 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;
|
||||
@@ -173,13 +173,12 @@ public final class MethodInfo {
|
||||
}
|
||||
|
||||
// If the receiver is parameterized (e.g., List<String>), substitute class type variables (E -> String)
|
||||
Map<String, TypeInfo> receiverBindings = null;
|
||||
GenericContext receiverCtx = null;
|
||||
if (GenericContext.hasGenerics(containingType)) {
|
||||
receiverBindings = TypeSubstitutor.createBindingsFromReceiver(containingType);
|
||||
if (!receiverBindings.isEmpty()) {
|
||||
returnType = TypeSubstitutor.substitute(returnType, receiverBindings);
|
||||
}
|
||||
receiverCtx = GenericContext.forReceiver(containingType);
|
||||
returnType = receiverCtx.substitute(returnType);
|
||||
}
|
||||
|
||||
List<FieldInfo> params = new ArrayList<>();
|
||||
// Use getGenericParameterTypes() to preserve generic information
|
||||
java.lang.reflect.Type[] genericParamTypes = method.getGenericParameterTypes();
|
||||
@@ -190,9 +189,8 @@ public final class MethodInfo {
|
||||
paramType = TypeInfo.fromClass(method.getParameterTypes()[i]);
|
||||
}
|
||||
|
||||
if (receiverBindings != null && !receiverBindings.isEmpty()) {
|
||||
paramType = TypeSubstitutor.substitute(paramType, receiverBindings);
|
||||
}
|
||||
if (receiverCtx != null)
|
||||
paramType = receiverCtx.substitute(paramType);
|
||||
|
||||
// Adapt Object params to receiver generic type args (e.g., Map.get(Object) -> Map.get(K))
|
||||
TypeInfo adapted = GenericParameterAdapter.adaptParameterType(method, containingType, i, paramType);
|
||||
@@ -377,6 +375,37 @@ public final class MethodInfo {
|
||||
*/
|
||||
public boolean hasInheritanceMarker() { return isOverride() || isImplements(); }
|
||||
|
||||
/**
|
||||
* Create a copy of this method with all type variables substituted using the given context.
|
||||
* Applies substitution to both the return type and every parameter type recursively.
|
||||
*
|
||||
* Used when a generic type is parameterized (e.g., {@code Box<String>}) and its raw methods
|
||||
* (declared with type variables like T) need to reflect the concrete type arguments.
|
||||
*
|
||||
* Example: for {@code class Box<T> { T get(); }}, calling this on the raw {@code get() → T}
|
||||
* with a context binding {@code T → String} produces {@code get() → String}.
|
||||
*
|
||||
* @param context the generic context containing type variable bindings; if null, returns {@code this} unchanged
|
||||
* @return a new MethodInfo with substituted types, or {@code this} if context is null
|
||||
*/
|
||||
public MethodInfo substituteTypeParams(GenericContext context) {
|
||||
if (context == null) {
|
||||
return this;
|
||||
}
|
||||
TypeInfo subReturnType = context.substitute(returnType);
|
||||
List<FieldInfo> subParams = new ArrayList<>(parameters.size());
|
||||
for (FieldInfo param : parameters) {
|
||||
subParams.add(param.substituteTypeParams(context));
|
||||
}
|
||||
MethodInfo result = new MethodInfo(name, subReturnType, containingType, subParams,
|
||||
fullDeclarationOffset, typeOffset, nameOffset, bodyStart, bodyEnd,
|
||||
resolved, isDeclaration, modifiers, documentation, javaMethod);
|
||||
result.jsDocInfo = this.jsDocInfo;
|
||||
result.overridesFrom = this.overridesFrom;
|
||||
result.implementsFrom = this.implementsFrom;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a position is inside this method's body.
|
||||
*/
|
||||
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.token;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import net.minecraft.client.resources.IResource;
|
||||
import net.minecraft.client.resources.IResourceManager;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import noppes.npcs.LogWriter;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manages reloadable style overrides (color, bold, italic) for {@link TokenType},
|
||||
* plus editor background/gutter/scrollbar colors via {@link BackgroundStyleEntry}.
|
||||
* <p>
|
||||
* Priority is always determined internally by {@link TokenType#getPriority()} ()}
|
||||
* and is never exposed to or loaded from user-facing JSON files.
|
||||
* <p>
|
||||
* Loaded from {@code assets/customnpcs/colorscheme/script_editor_scheme.json} on resource reload.
|
||||
* Styles are cached in a flat array indexed by {@link TokenType#ordinal()} for zero-cost lookups.
|
||||
*/
|
||||
public final class ScriptColorScheme {
|
||||
|
||||
// Volatile array reference swapped atomically — all tokens update at once
|
||||
static volatile StyleEntry[] styles = buildDefaults();
|
||||
|
||||
// Background/gutter/scrollbar colors — volatile for same atomic-swap pattern
|
||||
private static volatile BackgroundStyleEntry background = BackgroundStyleEntry.DEFAULT;
|
||||
|
||||
private ScriptColorScheme() {}
|
||||
|
||||
/** Returns the current background style (never null). */
|
||||
public static BackgroundStyleEntry getBackgroundStyle() {
|
||||
return background;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the color scheme from the resource pack's JSON file.
|
||||
* Uses {@code getAllResources()} to find the highest-priority version
|
||||
* (the last entry in the list, which is the latest resource pack override).
|
||||
* Falls back to defaults if the file is missing or malformed.
|
||||
*/
|
||||
public static void reloadColorScheme(IResourceManager resourceManager) {
|
||||
ResourceLocation loc = new ResourceLocation("customnpcs", "colorscheme/script_editor_scheme.json");
|
||||
try {
|
||||
List<IResource> resources = resourceManager.getAllResources(loc);
|
||||
IResource top = resources.get(resources.size() - 1);
|
||||
|
||||
try (InputStream is = top.getInputStream();
|
||||
InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
loadFromJson(reader);
|
||||
LogWriter.info("[ColorScheme] Loaded script_editor_scheme.json successfully");
|
||||
logLoadedSample();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
LogWriter.info("[ColorScheme] No script_editor_scheme.json found, using defaults");
|
||||
resetToDefaults();
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("[ColorScheme] Failed to load script_editor_scheme.json: " + e.getMessage());
|
||||
resetToDefaults();
|
||||
}
|
||||
}
|
||||
|
||||
private static void logLoadedSample() {
|
||||
StyleEntry[] current = styles;
|
||||
TokenType[] samples = { TokenType.COMMENT, TokenType.KEYWORD, TokenType.STRING, TokenType.METHOD_CALL, TokenType.DEFAULT };
|
||||
for (TokenType tt : samples) {
|
||||
StyleEntry e = current[tt.ordinal()];
|
||||
LogWriter.info("[ColorScheme] " + tt.name() + " -> color=#" + String.format("%08X", e.hexColor)
|
||||
+ ", bold=" + e.bold + ", italic=" + e.italic);
|
||||
}
|
||||
|
||||
BackgroundStyleEntry bg = background;
|
||||
LogWriter.info("[ColorScheme] background -> bg=#" + String.format("%08X", bg.getBackgroundColor())
|
||||
+ ", gutter=#" + String.format("%08X", bg.getGutterColor()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a color scheme JSON and install it as the active scheme.
|
||||
* Expected format: {@code { "TOKEN_NAME": { "color": "#RRGGBB", "bold": bool, "italic": bool }, ... }}
|
||||
* Missing properties per token fall back to the enum default. Parse errors are logged per-token.
|
||||
*/
|
||||
public static void loadFromJson(Reader reader) {
|
||||
try {
|
||||
JsonParser parser = new JsonParser();
|
||||
JsonElement root = parser.parse(reader);
|
||||
if (!root.isJsonObject()) {
|
||||
LogWriter.error("[ColorScheme] script_editor_scheme.json root is not a JSON object, using defaults");
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject obj = root.getAsJsonObject();
|
||||
|
||||
background = parseBackground(obj);
|
||||
|
||||
TokenType[] all = TokenType.values();
|
||||
StyleEntry[] next = new StyleEntry[all.length];
|
||||
for (TokenType tt : all) {
|
||||
next[tt.ordinal()] = parseEntry(obj, tt);
|
||||
}
|
||||
styles = next;
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("[ColorScheme] Failed to parse script_editor_scheme.json, using defaults: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetToDefaults() {
|
||||
styles = buildDefaults();
|
||||
background = BackgroundStyleEntry.DEFAULT;
|
||||
}
|
||||
|
||||
private static StyleEntry parseEntry(JsonObject root, TokenType tt) {
|
||||
if (!root.has(tt.name())) {
|
||||
return defaultEntry(tt);
|
||||
}
|
||||
try {
|
||||
JsonElement elem = root.get(tt.name());
|
||||
if (!elem.isJsonObject()) {
|
||||
LogWriter.error("[ColorScheme] Entry for " + tt.name() + " is not an object, skipping");
|
||||
return defaultEntry(tt);
|
||||
}
|
||||
JsonObject entry = elem.getAsJsonObject();
|
||||
|
||||
int defaultColor = tt.getDefaultHexColor();
|
||||
if ((defaultColor & 0xFF000000) == 0) {
|
||||
defaultColor |= 0xFF000000;
|
||||
}
|
||||
|
||||
int color = entry.has("color")
|
||||
? parseColor(entry.get("color"), defaultColor, tt.name())
|
||||
: defaultColor;
|
||||
boolean bold = entry.has("bold") ? entry.get("bold").getAsBoolean() : tt.getDefaultBold();
|
||||
boolean italic = entry.has("italic") ? entry.get("italic").getAsBoolean() : tt.getDefaultItalic();
|
||||
|
||||
return new StyleEntry(color, bold, italic);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("[ColorScheme] Error parsing token " + tt.name() + ", using defaults: " + e.getMessage());
|
||||
return defaultEntry(tt);
|
||||
}
|
||||
}
|
||||
|
||||
private static StyleEntry defaultEntry(TokenType tt) {
|
||||
int color = tt.getDefaultHexColor();
|
||||
if ((color & 0xFF000000) == 0) {
|
||||
color |= 0xFF000000;
|
||||
}
|
||||
return new StyleEntry(color, tt.getDefaultBold(), tt.getDefaultItalic());
|
||||
}
|
||||
|
||||
private static BackgroundStyleEntry parseBackground(JsonObject root) {
|
||||
if (!root.has("background")) {
|
||||
return BackgroundStyleEntry.DEFAULT;
|
||||
}
|
||||
try {
|
||||
JsonElement elem = root.get("background");
|
||||
if (!elem.isJsonObject()) {
|
||||
LogWriter.error("[ColorScheme] 'background' is not an object, using defaults");
|
||||
return BackgroundStyleEntry.DEFAULT;
|
||||
}
|
||||
JsonObject bg = elem.getAsJsonObject();
|
||||
BackgroundStyleEntry d = BackgroundStyleEntry.DEFAULT;
|
||||
|
||||
int backgroundColor = bg.has("backgroundColor") ? parseColor(bg.get("backgroundColor"), d.getBackgroundColor(), "background.backgroundColor") : d.getBackgroundColor();
|
||||
int gutterColor = bg.has("gutterColor") ? parseColor(bg.get("gutterColor"), d.getGutterColor(), "background.gutterColor") : d.getGutterColor();
|
||||
int gutterSeparatorColor = bg.has("gutterSeparatorColor") ? parseColor(bg.get("gutterSeparatorColor"), d.getGutterSeparatorColor(), "background.gutterSeparatorColor") : d.getGutterSeparatorColor();
|
||||
int lineNumberColor = bg.has("lineNumberColor") ? parseColor(bg.get("lineNumberColor"), d.getLineNumberColor(), "background.lineNumberColor") : d.getLineNumberColor();
|
||||
int lineNumberActiveColor = bg.has("lineNumberActiveColor") ? parseColor(bg.get("lineNumberActiveColor"), d.getLineNumberActiveColor(), "background.lineNumberActiveColor") : d.getLineNumberActiveColor();
|
||||
int scrollbarColor = bg.has("scrollbarColor") ? parseColor(bg.get("scrollbarColor"), d.getScrollbarColor(), "background.scrollbarColor") : d.getScrollbarColor();
|
||||
int borderColor = bg.has("borderColor") ? parseColor(bg.get("borderColor"), d.getBorderColor(), "background.borderColor") : d.getBorderColor();
|
||||
int caretColor = bg.has("caretColor") ? parseColor(bg.get("caretColor"), d.getCaretColor(), "background.caretColor") : d.getCaretColor();
|
||||
|
||||
return new BackgroundStyleEntry(backgroundColor, gutterColor, gutterSeparatorColor,
|
||||
lineNumberColor, lineNumberActiveColor, scrollbarColor, borderColor, caretColor);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("[ColorScheme] Error parsing 'background', using defaults: " + e.getMessage());
|
||||
return BackgroundStyleEntry.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static StyleEntry[] buildDefaults() {
|
||||
TokenType[] all = TokenType.values();
|
||||
StyleEntry[] defaults = new StyleEntry[all.length];
|
||||
for (TokenType tt : all) {
|
||||
defaults[tt.ordinal()] = defaultEntry(tt);
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
private static int parseColor(JsonElement element, int fallback, String tokenName) {
|
||||
try {
|
||||
if (element.isJsonPrimitive()) {
|
||||
if (element.getAsJsonPrimitive().isString()) {
|
||||
String s = element.getAsString().trim();
|
||||
if (s.startsWith("#")) {
|
||||
s = s.substring(1);
|
||||
}
|
||||
int color = (int) Long.parseLong(s, 16);
|
||||
if (s.length() <= 6) {
|
||||
color |= 0xFF000000;
|
||||
}
|
||||
return color;
|
||||
} else if (element.getAsJsonPrimitive().isNumber()) {
|
||||
return element.getAsInt();
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
LogWriter.error("[ColorScheme] Invalid color for " + tokenName + ": " + element + ", using default");
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static final class StyleEntry {
|
||||
final int hexColor;
|
||||
final boolean bold;
|
||||
final boolean italic;
|
||||
|
||||
StyleEntry(int hexColor, boolean bold, boolean italic) {
|
||||
this.hexColor = hexColor;
|
||||
this.bold = bold;
|
||||
this.italic = italic;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class BackgroundStyleEntry {
|
||||
static final BackgroundStyleEntry DEFAULT = new BackgroundStyleEntry(
|
||||
0xff000000, // backgroundColor (text viewport)
|
||||
0xff000000, // gutterColor (line-number gutter)
|
||||
0xff3c3f41, // gutterSeparatorColor
|
||||
0xFF606366, // lineNumberColor (inactive)
|
||||
0xFFb9c7d6, // lineNumberActiveColor (current line)
|
||||
0xFFe0e0e0, // scrollbarColor
|
||||
0xffa0a0a0, // borderColor (outer border)
|
||||
0xffffffff // caretColor (cursor)
|
||||
);
|
||||
|
||||
private final int backgroundColor;
|
||||
private final int gutterColor;
|
||||
private final int gutterSeparatorColor;
|
||||
private final int lineNumberColor;
|
||||
private final int lineNumberActiveColor;
|
||||
private final int scrollbarColor;
|
||||
private final int borderColor;
|
||||
private final int caretColor;
|
||||
|
||||
BackgroundStyleEntry(int backgroundColor, int gutterColor, int gutterSeparatorColor,
|
||||
int lineNumberColor, int lineNumberActiveColor,
|
||||
int scrollbarColor, int borderColor, int caretColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
this.gutterColor = gutterColor;
|
||||
this.gutterSeparatorColor = gutterSeparatorColor;
|
||||
this.lineNumberColor = lineNumberColor;
|
||||
this.lineNumberActiveColor = lineNumberActiveColor;
|
||||
this.scrollbarColor = scrollbarColor;
|
||||
this.borderColor = borderColor;
|
||||
this.caretColor = caretColor;
|
||||
}
|
||||
|
||||
public int getBackgroundColor() { return backgroundColor; }
|
||||
public int getGutterColor() { return gutterColor; }
|
||||
public int getGutterSeparatorColor() { return gutterSeparatorColor; }
|
||||
public int getLineNumberColor() { return lineNumberColor; }
|
||||
public int getLineNumberActiveColor() { return lineNumberActiveColor; }
|
||||
public int getScrollbarColor() { return scrollbarColor; }
|
||||
public int getBorderColor() { return borderColor; }
|
||||
public int getCaretColor() { return caretColor; }
|
||||
}
|
||||
}
|
||||
@@ -58,10 +58,6 @@ public class Token {
|
||||
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);
|
||||
}
|
||||
@@ -196,8 +192,7 @@ public class Token {
|
||||
// ==================== TYPE CHECKS ====================
|
||||
|
||||
public boolean isKeyword() {
|
||||
return type == TokenType.KEYWORD || type == TokenType.MODIFIER ||
|
||||
type == TokenType.CLASS_KEYWORD || type == TokenType.IMPORT_KEYWORD;
|
||||
return type == TokenType.KEYWORD;
|
||||
}
|
||||
|
||||
public boolean isTypeReference() {
|
||||
@@ -264,6 +259,18 @@ public class Token {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the java.awt.Font style bitmask for this token.
|
||||
* Returns a combination of {@link java.awt.Font#PLAIN}, {@link java.awt.Font#BOLD},
|
||||
* and {@link java.awt.Font#ITALIC}.
|
||||
*/
|
||||
public int getFontStyle() {
|
||||
int style = java.awt.Font.PLAIN;
|
||||
if (type.isBold()) style |= java.awt.Font.BOLD;
|
||||
if (type.isItalic()) style |= java.awt.Font.ITALIC;
|
||||
return style;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Token{'" + text + "', " + type + ", [" + globalStart + "-" + globalEnd + "]}";
|
||||
|
||||
@@ -1,74 +1,76 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.token;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.field.FieldInfo;
|
||||
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.
|
||||
* <p>
|
||||
* Priority is defined exclusively by the enum and never stored elsewhere.
|
||||
* Visual style properties (color, bold, italic) can be overridden at runtime
|
||||
* via {@link ScriptColorScheme}. The enum constructor values serve as compile-time defaults.
|
||||
*/
|
||||
public enum TokenType {
|
||||
// Comments and strings have highest priority - they override everything inside them
|
||||
COMMENT(0x777777, 140),
|
||||
STRING(0xCC8855, 130),
|
||||
COMMENT(0xFF777777, 140),
|
||||
STRING(0xFF17C6A3, 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)
|
||||
JSDOC_TAG(0xFF8E8070, 125,true, true), // @param, @type, @return etc. (gold/orange)
|
||||
JSDOC_TYPE(0xFF9A8C7C, 124), // {TypeName} in JSDoc (aqua like types)
|
||||
|
||||
UNUSED_IMPORT(0x666666, 119), // unused import statements (gray)
|
||||
UNUSED_IMPORT(0xFF666666, 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.
|
||||
KEYWORD(0xFFFF5555, 100,true, false), // control flow: if, else, for, while, 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
|
||||
GENERIC_TYPE_PARAM(0x00FA9A, 76), // generic type parameters like T, E, K, V (light green)
|
||||
TYPE_DECL(0x00AAAA, 70), // package paths, type references
|
||||
INTERFACE_DECL(0xFF80F2F6, 85), // interface names (aqua)
|
||||
ENUM_DECL(0xFFFF55FF, 85), // enum names (magenta)
|
||||
ENUM_CONSTANT(0xFF55FFFF, 84, true, true), // enum constant values (blue, bold+italic) - like IntelliJ
|
||||
CLASS_DECL(0xFF00AAAA, 85), // class names in declarations
|
||||
IMPORTED_CLASS(0xFF00AAAA, 75), // imported class usages
|
||||
GENERIC_TYPE_PARAM(0xFF00FA9A, 76), // generic type parameters like T, E, K, V (light green)
|
||||
TYPE_DECL(0xFF00AAAA, 70), // package paths, type references
|
||||
|
||||
// Methods
|
||||
METHOD_DECL(0x00AA00, 60), // method declarations (green)
|
||||
METHOD_CALL(0x55FF55, 50), // method calls (bright green)
|
||||
METHOD_DECL(0xFF00AA00, 60), // method declarations (green)
|
||||
METHOD_CALL(0xFF55FF55, 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)
|
||||
UNDEFINED_VAR(0xFFAA0000, 20), // unresolved variables (dark red) - high priority
|
||||
PARAMETER(0xFF5555FF, 36), // method parameters (blue)
|
||||
GLOBAL_FIELD(0xFF55FFFF, 35), // class-level fields (aqua)
|
||||
LOCAL_FIELD(0xFFFFFF55, 25), // local variables (yellow)
|
||||
STATIC_FINAL_FIELD(0xFFFF55FF, 36, true, true), // static final fields (magenta, italic)
|
||||
|
||||
// Literals
|
||||
LITERAL(0x777777, 40), // numeric and boolean literals
|
||||
LITERAL(0xFF79C0FF, 40), // numeric and boolean literals
|
||||
|
||||
// Default
|
||||
VARIABLE(0xFFFFFF, 30), // generic variables
|
||||
DEFAULT(0xFFFFFF, 0); // default text color (white)
|
||||
VARIABLE(0xFFFFFFFF, 30), // generic variables
|
||||
DEFAULT(0xFFFFFFFF, 0); // default text color (white)
|
||||
|
||||
private final int hexColor;
|
||||
private final int defaultHexColor;
|
||||
private final int priority;
|
||||
private final boolean bold;
|
||||
private final boolean italic;
|
||||
private final boolean defaultBold;
|
||||
private final boolean defaultItalic;
|
||||
|
||||
TokenType(int hexColor, int priority) {
|
||||
this(hexColor, priority, false, false);
|
||||
}
|
||||
|
||||
TokenType(int hexColor, int priority, boolean bold, boolean italic) {
|
||||
this.hexColor = hexColor;
|
||||
this.defaultHexColor = hexColor;
|
||||
this.priority = priority;
|
||||
this.bold = bold;
|
||||
this.italic = italic;
|
||||
this.defaultBold = bold;
|
||||
this.defaultItalic = italic;
|
||||
}
|
||||
|
||||
public int getHexColor() {
|
||||
return hexColor;
|
||||
return ScriptColorScheme.styles[ordinal()].hexColor;
|
||||
}
|
||||
|
||||
public int getPriority() {
|
||||
@@ -76,11 +78,23 @@ public enum TokenType {
|
||||
}
|
||||
|
||||
public boolean isBold() {
|
||||
return bold;
|
||||
return ScriptColorScheme.styles[ordinal()].bold;
|
||||
}
|
||||
|
||||
public boolean isItalic() {
|
||||
return italic;
|
||||
return ScriptColorScheme.styles[ordinal()].italic;
|
||||
}
|
||||
|
||||
int getDefaultHexColor() {
|
||||
return defaultHexColor;
|
||||
}
|
||||
|
||||
boolean getDefaultBold() {
|
||||
return defaultBold;
|
||||
}
|
||||
|
||||
boolean getDefaultItalic() {
|
||||
return defaultItalic;
|
||||
}
|
||||
|
||||
public static TokenType getByType(TypeInfo typeInfo) {
|
||||
@@ -97,7 +111,10 @@ public enum TokenType {
|
||||
public static int getColor(TypeInfo typeInfo) {
|
||||
return getByType(typeInfo).getHexColor();
|
||||
}
|
||||
|
||||
|
||||
public static int getPackageColor(){
|
||||
return IMPORTED_CLASS.getHexColor();
|
||||
}
|
||||
/**
|
||||
* Convert this token type to a Minecraft color code character.
|
||||
* Used for backward compatibility with the existing rendering system.
|
||||
@@ -114,12 +131,8 @@ public enum TokenType {
|
||||
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
|
||||
|
||||
+15
-16
@@ -8,12 +8,16 @@ 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.
|
||||
* Single source of truth for generic type variable substitution.
|
||||
*
|
||||
* 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).
|
||||
* Resolves type parameters (T, E, K, V...) to concrete types using:
|
||||
* 1. Applied type arguments (e.g., List<String> binds E → String) — priority
|
||||
* 2. Declared bounds fallback (e.g., <T extends Entity> binds T → Entity) — fallback
|
||||
*
|
||||
* Usage patterns:
|
||||
* - {@link #forReceiver(TypeInfo)} — build context from a parameterized receiver type
|
||||
* - {@link #fromBindings(List, List)} — build context from explicit params/args (anti-recursion safe)
|
||||
* - {@link #substitute(TypeInfo)} — instance method with two-tier resolution
|
||||
*/
|
||||
public final class GenericContext {
|
||||
|
||||
@@ -73,8 +77,9 @@ public final class GenericContext {
|
||||
if (declared == null) continue;
|
||||
String name = declared.getName();
|
||||
if (name == null || name.isEmpty()) continue;
|
||||
TypeInfo bound = declared.getBoundTypeInfo();
|
||||
bounds.put(name, (bound != null && bound.isResolved()) ? bound : TypeInfo.OBJECT);
|
||||
TypeInfo tp = TypeInfo.typeParameter(name, declared);
|
||||
TypeInfo effectiveBound = tp.getBoundType();
|
||||
bounds.put(name, effectiveBound != null ? effectiveBound : TypeInfo.OBJECT);
|
||||
}
|
||||
|
||||
return new GenericContext(applied, bounds);
|
||||
@@ -125,15 +130,9 @@ public final class GenericContext {
|
||||
if (declared == null) continue;
|
||||
String name = declared.getName();
|
||||
if (name == null || name.isEmpty()) continue;
|
||||
TypeInfo bound = declared.getBoundTypeInfo();
|
||||
TypeInfo effectiveBound = (bound != null && bound.isResolved()) ? bound : TypeInfo.OBJECT;
|
||||
|
||||
// TODO: Goatee not sure what this was for
|
||||
// List<TypeInfo> additionalBounds = declared.getAdditionalBoundTypes();
|
||||
// if (additionalBounds != null && !additionalBounds.isEmpty()) {
|
||||
// effectiveBound = IntersectionTypeInfo.of(effectiveBound, additionalBounds);
|
||||
// }
|
||||
bounds.put(name, effectiveBound);
|
||||
TypeInfo tp = TypeInfo.typeParameter(name, declared);
|
||||
TypeInfo effectiveBound = tp.getBoundType();
|
||||
bounds.put(name, effectiveBound != null ? effectiveBound : TypeInfo.OBJECT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
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.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Represents an intersection type from multiple bounds on a type parameter.
|
||||
* For example, {@code <T extends Number & Comparable<T>>} creates an intersection
|
||||
* of Number and Comparable.
|
||||
*
|
||||
* <p>The primary bound determines the display name and main type identity.
|
||||
* Member lookup (hasMethod, getMethodInfo, hasField, getFieldInfo, getAllMethodOverloads)
|
||||
* delegates to ALL constituent types, so methods from any bound are accessible.</p>
|
||||
*
|
||||
* <p>This is used by {@link GenericContext} when building bound fallbacks for type
|
||||
* parameters with additional interface bounds.</p>
|
||||
*/
|
||||
public class IntersectionTypeInfo extends TypeInfo {
|
||||
|
||||
/** The primary (first) bound type — used for display and as the "main" type. */
|
||||
private final TypeInfo primaryBound;
|
||||
|
||||
/** Additional interface bounds (everything after {@code &}). */
|
||||
private final List<TypeInfo> additionalBounds;
|
||||
|
||||
private IntersectionTypeInfo(TypeInfo primaryBound, List<TypeInfo> additionalBounds) {
|
||||
super(primaryBound.getSimpleName(), primaryBound.getFullName(), "",
|
||||
primaryBound.getKind(), primaryBound.getJavaClass(), true, null, true);
|
||||
this.primaryBound = primaryBound;
|
||||
this.additionalBounds = additionalBounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an intersection type from a primary bound and additional interface bounds.
|
||||
* If there are no additional bounds, returns the primary bound directly (no wrapper).
|
||||
*
|
||||
* @param primary the primary (first) bound type
|
||||
* @param additional the additional interface bounds (may be empty)
|
||||
* @return an IntersectionTypeInfo if there are additional bounds, otherwise the primary itself
|
||||
*/
|
||||
public static TypeInfo of(TypeInfo primary, List<TypeInfo> additional) {
|
||||
if (additional == null || additional.isEmpty()) {
|
||||
return primary;
|
||||
}
|
||||
return new IntersectionTypeInfo(primary, new ArrayList<>(additional));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMethod(String methodName) {
|
||||
if (primaryBound.hasMethod(methodName)) return true;
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
if (bound.hasMethod(methodName)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMethod(String methodName, int paramCount) {
|
||||
if (primaryBound.hasMethod(methodName, paramCount)) return true;
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
if (bound.hasMethod(methodName, paramCount)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodInfo getMethodInfo(String methodName) {
|
||||
MethodInfo m = primaryBound.getMethodInfo(methodName);
|
||||
if (m != null) return m;
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
m = bound.getMethodInfo(methodName);
|
||||
if (m != null) return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MethodInfo> getAllMethodOverloads(String methodName) {
|
||||
Set<MethodInfo> seen = new LinkedHashSet<>();
|
||||
seen.addAll(primaryBound.getAllMethodOverloads(methodName));
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
seen.addAll(bound.getAllMethodOverloads(methodName));
|
||||
}
|
||||
return new ArrayList<>(seen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodInfo getBestMethodOverload(String methodName, TypeInfo[] argTypes) {
|
||||
MethodInfo m = primaryBound.getBestMethodOverload(methodName, argTypes);
|
||||
if (m != null) return m;
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
m = bound.getBestMethodOverload(methodName, argTypes);
|
||||
if (m != null) return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodInfo getBestMethodOverload(String methodName, TypeInfo expectedReturnType) {
|
||||
MethodInfo m = primaryBound.getBestMethodOverload(methodName, expectedReturnType);
|
||||
if (m != null) return m;
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
m = bound.getBestMethodOverload(methodName, expectedReturnType);
|
||||
if (m != null) return m;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasField(String fieldName) {
|
||||
if (primaryBound.hasField(fieldName)) return true;
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
if (bound.hasField(fieldName)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FieldInfo getFieldInfo(String fieldName) {
|
||||
FieldInfo f = primaryBound.getFieldInfo(fieldName);
|
||||
if (f != null) return f;
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
f = bound.getFieldInfo(fieldName);
|
||||
if (f != null) return f;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public TypeInfo getPrimaryBound() {
|
||||
return primaryBound;
|
||||
}
|
||||
|
||||
public List<TypeInfo> getAdditionalBounds() {
|
||||
return new ArrayList<>(additionalBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
StringBuilder sb = new StringBuilder(primaryBound.getDisplayName());
|
||||
for (TypeInfo bound : additionalBounds) {
|
||||
sb.append(" & ").append(bound.getDisplayName());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isResolved() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IntersectionTypeInfo{" + getDisplayName() + "}";
|
||||
}
|
||||
}
|
||||
+60
-35
@@ -10,6 +10,7 @@ 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.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -152,32 +153,12 @@ public class ScriptTypeInfo extends TypeInfo {
|
||||
return super.getTypeParam(name);
|
||||
}
|
||||
|
||||
private static MethodInfo substituteMethod(MethodInfo m, GenericContext ctx) {
|
||||
TypeInfo retType = m.getReturnType();
|
||||
TypeInfo subRet = ctx.substitute(retType);
|
||||
List<FieldInfo> params = new ArrayList<>(m.getParameters());
|
||||
boolean changed = subRet != retType;
|
||||
for (int i = 0; i < params.size(); i++) {
|
||||
FieldInfo p = params.get(i);
|
||||
TypeInfo subP = ctx.substitute(p.getTypeInfo());
|
||||
if (subP != p.getTypeInfo()) {
|
||||
params.set(i, FieldInfo.reflectionParam(p.getName(), subP));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? MethodInfo.external(m.getName(), subRet, m.getContainingType(),
|
||||
params, m.getModifiers(), m.getDocumentation()) : m;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public TypeInfo parameterize(List<TypeInfo> typeArgs) {
|
||||
if (typeArgs == null || typeArgs.isEmpty()) return this;
|
||||
|
||||
Set<ScriptTypeInfo> active = PARAMETERIZING.get();
|
||||
if (!active.add(this)) {
|
||||
// Re-entrant call on the same raw type — fall back to shallow parameterization
|
||||
// (no method/field substitution) to break the infinite recursion.
|
||||
return super.parameterize(typeArgs);
|
||||
}
|
||||
|
||||
@@ -199,17 +180,14 @@ public class ScriptTypeInfo extends TypeInfo {
|
||||
// Parameterize methods, constructors, and fields with the generic context of this type.
|
||||
for (Map.Entry<String, List<MethodInfo>> entry : this.methods.entrySet()) {
|
||||
List<MethodInfo> substituted = new ArrayList<>();
|
||||
for (MethodInfo m : entry.getValue()) substituted.add(substituteMethod(m, ctx));
|
||||
for (MethodInfo m : entry.getValue()) substituted.add(m.substituteTypeParams(ctx));
|
||||
result.methods.put(entry.getKey(), substituted);
|
||||
}
|
||||
for (MethodInfo ctor : this.constructors) {
|
||||
result.constructors.add(substituteMethod(ctor, ctx));
|
||||
result.constructors.add(ctor.substituteTypeParams(ctx));
|
||||
}
|
||||
for (Map.Entry<String, FieldInfo> entry : this.fields.entrySet()) {
|
||||
FieldInfo f = entry.getValue();
|
||||
TypeInfo subType = ctx.substitute(f.getTypeInfo());
|
||||
result.fields.put(entry.getKey(), subType != f.getTypeInfo()
|
||||
? FieldInfo.external(f.getName(), subType, null, f.getModifiers()) : f);
|
||||
result.fields.put(entry.getKey(), entry.getValue().substituteTypeParams(ctx));
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
@@ -310,7 +288,11 @@ public class ScriptTypeInfo extends TypeInfo {
|
||||
|
||||
if (enumConstants.containsKey(fieldName))
|
||||
return true;
|
||||
|
||||
|
||||
for (FieldInfo f : getSyntheticFields())
|
||||
if (f.getName().equals(fieldName))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -325,6 +307,10 @@ public class ScriptTypeInfo extends TypeInfo {
|
||||
return enumConst.getFieldInfo();
|
||||
}
|
||||
|
||||
for (FieldInfo f : getSyntheticFields())
|
||||
if (f.getName().equals(fieldName))
|
||||
return f;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -361,6 +347,7 @@ public class ScriptTypeInfo extends TypeInfo {
|
||||
/**
|
||||
* Get all enum constants.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, EnumConstantInfo> getEnumConstants() {
|
||||
return new HashMap<>(enumConstants);
|
||||
}
|
||||
@@ -380,32 +367,51 @@ public class ScriptTypeInfo extends TypeInfo {
|
||||
|
||||
@Override
|
||||
public boolean hasMethod(String methodName) {
|
||||
return methods.containsKey(methodName);
|
||||
if (methods.containsKey(methodName))
|
||||
return true;
|
||||
for (MethodInfo m : getSyntheticMethods())
|
||||
if (m.getName().equals(methodName))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@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;
|
||||
if (overloads != null) {
|
||||
for (MethodInfo m : overloads) {
|
||||
if (m.getParameterCount() == paramCount) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (MethodInfo m : getSyntheticMethods())
|
||||
if (m.getName().equals(methodName) && 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;
|
||||
if (overloads != null && !overloads.isEmpty())
|
||||
return overloads.get(0);
|
||||
for (MethodInfo m : getSyntheticMethods())
|
||||
if (m.getName().equals(methodName))
|
||||
return m;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all method overloads with the given name.
|
||||
*/
|
||||
@Override
|
||||
public List<MethodInfo> getAllMethodOverloads(String methodName) {
|
||||
return methods.getOrDefault(methodName, new ArrayList<>());
|
||||
List<MethodInfo> result = new ArrayList<>(methods.getOrDefault(methodName, Collections.emptyList()));
|
||||
for (MethodInfo m : getSyntheticMethods())
|
||||
if (m.getName().equals(methodName))
|
||||
result.add(m);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,7 +438,26 @@ public class ScriptTypeInfo extends TypeInfo {
|
||||
allMethods.addAll(overloads);
|
||||
}
|
||||
return allMethods;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MethodInfo> getAllMethods() {
|
||||
List<MethodInfo> result = getAllMethodsFlat();
|
||||
result.addAll(getSyntheticMethods());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FieldInfo> getAllFields() {
|
||||
List<FieldInfo> result = new ArrayList<>(getFields().values());
|
||||
result.addAll(getSyntheticFields());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TypeInfo> getAllNestedTypes() {
|
||||
return new ArrayList<>(innerClasses);
|
||||
}
|
||||
|
||||
// ==================== CONSTRUCTOR MANAGEMENT ====================
|
||||
|
||||
|
||||
@@ -194,12 +194,74 @@ public class TypeInfo {
|
||||
public static TypeInfo typeParameter(String paramName, TypeInfo boundType) {
|
||||
Class<?> javaClass = (boundType != null && boundType.javaClass != null) ? boundType.javaClass : Object.class;
|
||||
Kind kind = (boundType != null) ? boundType.kind : Kind.CLASS;
|
||||
|
||||
if (boundType instanceof IntersectionTypeInfo) {
|
||||
final IntersectionTypeInfo intersection = (IntersectionTypeInfo) boundType;
|
||||
TypeInfo ti = new TypeInfo(paramName, paramName, "", kind, javaClass, true, null) {
|
||||
@Override
|
||||
public List<MethodInfo> getAllMethods() {
|
||||
List<MethodInfo> result = new ArrayList<>(super.getAllMethods());
|
||||
for (TypeInfo bound : intersection.getAdditionalBounds())
|
||||
result.addAll(bound.getAllMethods());
|
||||
|
||||
return result;
|
||||
}
|
||||
@Override
|
||||
public List<FieldInfo> getAllFields() {
|
||||
List<FieldInfo> result = new ArrayList<>(super.getAllFields());
|
||||
for (TypeInfo bound : intersection.getAdditionalBounds())
|
||||
result.addAll(bound.getAllFields());
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
ti.typeParameterName = paramName;
|
||||
ti.boundType = boundType;
|
||||
return ti;
|
||||
}
|
||||
|
||||
TypeInfo ti = new TypeInfo(paramName, paramName, "", kind, javaClass, true, null);
|
||||
ti.typeParameterName = paramName;
|
||||
ti.boundType = boundType;
|
||||
return ti;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TypeInfo representing a bounded type parameter from a {@link TypeParamInfo}.
|
||||
* This is the single point of control for creating type parameter TypeInfos with bound handling.
|
||||
*
|
||||
* <p>If the TypeParamInfo has additional bounds (e.g., {@code T extends Number & Comparable<T>}),
|
||||
* an {@link IntersectionTypeInfo} is created internally to represent the combined bound.
|
||||
* This centralizes intersection creation so no caller needs to call {@code IntersectionTypeInfo.of()} directly.</p>
|
||||
*
|
||||
* @param paramName the type parameter name (e.g., "T")
|
||||
* @param typeParam the TypeParamInfo containing bound information
|
||||
* @return a TypeInfo tagged as a type parameter with the effective bound
|
||||
*/
|
||||
public static TypeInfo typeParameter(String paramName, TypeParamInfo typeParam) {
|
||||
if (typeParam == null) {
|
||||
return typeParameter(paramName);
|
||||
}
|
||||
TypeInfo primaryBound = typeParam.getBoundTypeInfo();
|
||||
if (primaryBound == null) {
|
||||
typeParam.resolveBoundType();
|
||||
primaryBound = typeParam.getBoundTypeInfo();
|
||||
}
|
||||
|
||||
TypeInfo effectiveBound;
|
||||
if (primaryBound != null && primaryBound.isResolved()) {
|
||||
List<TypeInfo> additionalBounds = typeParam.getAdditionalBoundTypes();
|
||||
if (additionalBounds != null && !additionalBounds.isEmpty()) {
|
||||
effectiveBound = IntersectionTypeInfo.of(primaryBound, additionalBounds);
|
||||
} else {
|
||||
effectiveBound = primaryBound;
|
||||
}
|
||||
} else {
|
||||
effectiveBound = null;
|
||||
}
|
||||
return typeParameter(paramName, effectiveBound);
|
||||
}
|
||||
|
||||
public boolean isTypeParameter() {
|
||||
return typeParameterName != null;
|
||||
}
|
||||
@@ -542,6 +604,7 @@ public class TypeInfo {
|
||||
public boolean isInnerClass() { return enclosingType != null; }
|
||||
public boolean isInterface() {return kind == Kind.INTERFACE;}
|
||||
public boolean isEnum() {return kind == Kind.ENUM;}
|
||||
public boolean isClass() {return kind == Kind.CLASS;}
|
||||
public boolean isPrimitive() {return this.isPrimitive || javaClass != null && javaClass.isPrimitive();}
|
||||
private TypeInfo setPrimitive(boolean value) { this.isPrimitive = value; return this;}
|
||||
|
||||
@@ -709,6 +772,145 @@ public class TypeInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all public instance members of this type for autocomplete/resolution.
|
||||
* Override in subclasses to extend or replace the default Java reflection path.
|
||||
*/
|
||||
public List<MethodInfo> getAllMethods() {
|
||||
if (javaClass != null) {
|
||||
// Java reflection path: enumerate public methods + synthetics
|
||||
List<MethodInfo> result = new ArrayList<>();
|
||||
try {
|
||||
for (java.lang.reflect.Method m : javaClass.getMethods()) {
|
||||
try {
|
||||
result.add(MethodInfo.fromReflection(m, this));
|
||||
} catch (Exception e) {
|
||||
// Skip individual methods that fail (e.g., linkage errors on obscure return types)
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
result.addAll(getSyntheticMethods());
|
||||
return result;
|
||||
}
|
||||
|
||||
// javaClass == null: check rawType, jsTypeInfo, synthetics in priority order
|
||||
if (rawType != null && rawType != this) {
|
||||
List<MethodInfo> rawMethods = rawType.getAllMethods();
|
||||
if (!rawMethods.isEmpty() && isParameterized()) {
|
||||
GenericContext ctx = GenericContext.forReceiver(this);
|
||||
List<MethodInfo> substituted = new ArrayList<>(rawMethods.size());
|
||||
for (MethodInfo m : rawMethods) {
|
||||
substituted.add(m.substituteTypeParams(ctx));
|
||||
}
|
||||
return substituted;
|
||||
}
|
||||
return rawMethods;
|
||||
}
|
||||
|
||||
// No javaClass, no rawType — collect from jsTypeInfo and synthetics
|
||||
List<MethodInfo> result = new ArrayList<>();
|
||||
if (jsTypeInfo != null) {
|
||||
for (JSMethodInfo m : jsTypeInfo.getMethods().values()) {
|
||||
result.add(MethodInfo.fromJSMethod(m, this));
|
||||
}
|
||||
}
|
||||
result.addAll(getSyntheticMethods());
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<FieldInfo> getAllFields() {
|
||||
if (javaClass != null) {
|
||||
// Java reflection path: enumerate public fields + synthetics
|
||||
List<FieldInfo> result = new ArrayList<>();
|
||||
try {
|
||||
for (java.lang.reflect.Field f : javaClass.getFields()) {
|
||||
try {
|
||||
result.add(FieldInfo.fromReflection(f, this));
|
||||
} catch (Exception e) {
|
||||
// Skip individual fields that fail rather than losing ALL fields.
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
result.addAll(getSyntheticFields());
|
||||
return result;
|
||||
}
|
||||
|
||||
// javaClass == null: check rawType, jsTypeInfo, synthetics in priority order
|
||||
if (rawType != null && rawType != this) {
|
||||
List<FieldInfo> rawFields = rawType.getAllFields();
|
||||
if (!rawFields.isEmpty() && isParameterized()) {
|
||||
GenericContext ctx = GenericContext.forReceiver(this);
|
||||
List<FieldInfo> substituted = new ArrayList<>(rawFields.size());
|
||||
for (FieldInfo f : rawFields) {
|
||||
substituted.add(f.substituteTypeParams(ctx));
|
||||
}
|
||||
return substituted;
|
||||
}
|
||||
return rawFields;
|
||||
}
|
||||
|
||||
// No javaClass, no rawType — collect from jsTypeInfo and synthetics
|
||||
List<FieldInfo> result = new ArrayList<>();
|
||||
if (jsTypeInfo != null) {
|
||||
for (JSFieldInfo f : jsTypeInfo.getFields().values()) {
|
||||
result.add(FieldInfo.fromJSField(f, this));
|
||||
}
|
||||
}
|
||||
result.addAll(getSyntheticFields());
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all public nested types (inner classes/interfaces/enums) of this type.
|
||||
* Uses reflection to discover nested types from the Java class, filters to public only,
|
||||
* converts internal '$' separators to '.' for name resolution, and resolves each via TypeResolver.
|
||||
*
|
||||
* Override in subclasses (e.g., ScriptTypeInfo) to return script-defined inner classes.
|
||||
*
|
||||
* @return List of resolved nested TypeInfo instances (may be empty, never null)
|
||||
*/
|
||||
public List<TypeInfo> getAllNestedTypes() {
|
||||
if (javaClass != null) {
|
||||
List<TypeInfo> result = new ArrayList<>();
|
||||
try {
|
||||
for (Class<?> nested : javaClass.getDeclaredClasses()) {
|
||||
if (!Modifier.isPublic(nested.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
// Convert JVM internal '$' separator to '.' for TypeResolver lookup
|
||||
String nestedFullName = nested.getName().replace('$', '.');
|
||||
TypeInfo nestedType = TypeResolver.getInstance().resolveFullName(nestedFullName);
|
||||
if (nestedType != null && nestedType.isResolved()) {
|
||||
result.add(nestedType);
|
||||
}
|
||||
}
|
||||
} catch (SecurityException ignored) {
|
||||
// Some runtimes may restrict getDeclaredClasses()
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (rawType != null && rawType != this) {
|
||||
return rawType.getAllNestedTypes();
|
||||
}
|
||||
|
||||
// No javaClass, no rawType — check jsTypeInfo for inner types
|
||||
if (jsTypeInfo != null) {
|
||||
Map<String, JSTypeInfo> innerTypes = jsTypeInfo.getInnerTypes();
|
||||
if (!innerTypes.isEmpty()) {
|
||||
List<TypeInfo> result = new ArrayList<>(innerTypes.size());
|
||||
for (JSTypeInfo inner : innerTypes.values()) {
|
||||
TypeInfo ti = TypeInfo.fromJSTypeInfo(inner);
|
||||
if (ti != null) {
|
||||
result.add(ti);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this type has a method with the given name.
|
||||
*/
|
||||
@@ -727,6 +929,8 @@ public class TypeInfo {
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} else if (rawType != null && rawType != this) {
|
||||
return rawType.hasMethod(methodName);
|
||||
}
|
||||
|
||||
TypeInfo obj = TypeInfo.object();
|
||||
@@ -755,6 +959,8 @@ public class TypeInfo {
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} else if (rawType != null && rawType != this) {
|
||||
return rawType.hasMethod(methodName, paramCount);
|
||||
}
|
||||
TypeInfo obj = TypeInfo.object();
|
||||
if (obj != this) return obj.hasMethod(methodName);
|
||||
@@ -766,13 +972,15 @@ public class TypeInfo {
|
||||
* Override in ScriptTypeInfo for script-defined types.
|
||||
*/
|
||||
public boolean hasConstructors() {
|
||||
if (javaClass == null) return false;
|
||||
try {
|
||||
return javaClass.getConstructors().length > 0;
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
return false;
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
return javaClass.getConstructors().length > 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (rawType != null && rawType != this) return rawType.hasConstructors();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -781,16 +989,18 @@ public class TypeInfo {
|
||||
*/
|
||||
public List<MethodInfo> getConstructors() {
|
||||
List<MethodInfo> result = new ArrayList<>();
|
||||
if (javaClass == null) return result;
|
||||
|
||||
try {
|
||||
java.lang.reflect.Constructor<?>[] constructors = javaClass.getConstructors();
|
||||
for (java.lang.reflect.Constructor<?> ctor : constructors) {
|
||||
result.add(MethodInfo.fromReflectionConstructor(ctor, this));
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
java.lang.reflect.Constructor<?>[] constructors = javaClass.getConstructors();
|
||||
for (java.lang.reflect.Constructor<?> ctor : constructors) {
|
||||
result.add(MethodInfo.fromReflectionConstructor(ctor, this));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
return result;
|
||||
}
|
||||
if (rawType != null && rawType != this) return rawType.getConstructors();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -799,45 +1009,49 @@ public class TypeInfo {
|
||||
* Override in ScriptTypeInfo for script-defined types.
|
||||
*/
|
||||
public MethodInfo findConstructor(int argCount) {
|
||||
if (javaClass == null) return null;
|
||||
|
||||
try {
|
||||
Constructor<?>[] constructors = javaClass.getConstructors();
|
||||
for (Constructor<?> ctor : constructors) {
|
||||
if (ctor.getParameterCount() == argCount) {
|
||||
return MethodInfo.fromReflectionConstructor(ctor, this);
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
Constructor<?>[] constructors = javaClass.getConstructors();
|
||||
for (Constructor<?> ctor : constructors) {
|
||||
if (ctor.getParameterCount() == argCount) {
|
||||
return MethodInfo.fromReflectionConstructor(ctor, this);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
return null;
|
||||
}
|
||||
if (rawType != null && rawType != this) return rawType.findConstructor(argCount);
|
||||
return null;
|
||||
}
|
||||
|
||||
public MethodInfo findConstructor(TypeInfo[] argTypes) {
|
||||
if (javaClass == null) return null;
|
||||
|
||||
try {
|
||||
Constructor<?>[] constructors = javaClass.getConstructors();
|
||||
for (Constructor<?> ctor : constructors) {
|
||||
if (ctor.getParameterCount() == argTypes.length) {
|
||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
||||
boolean match = true;
|
||||
for (int i = 0; i < argTypes.length; i++) {
|
||||
TypeInfo paramTypeInfo = TypeInfo.fromClass(paramTypes[i]);
|
||||
if (!TypeChecker.isTypeCompatible(paramTypeInfo, argTypes[i])) {
|
||||
match = false;
|
||||
break;
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
Constructor<?>[] constructors = javaClass.getConstructors();
|
||||
for (Constructor<?> ctor : constructors) {
|
||||
if (ctor.getParameterCount() == argTypes.length) {
|
||||
Class<?>[] paramTypes = ctor.getParameterTypes();
|
||||
boolean match = true;
|
||||
for (int i = 0; i < argTypes.length; i++) {
|
||||
TypeInfo paramTypeInfo = TypeInfo.fromClass(paramTypes[i]);
|
||||
if (!TypeChecker.isTypeCompatible(paramTypeInfo, argTypes[i])) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return MethodInfo.fromReflectionConstructor(ctor, this);
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
return MethodInfo.fromReflectionConstructor(ctor, this);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
return null;
|
||||
}
|
||||
if (rawType != null && rawType != this) return rawType.findConstructor(argTypes);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -851,30 +1065,63 @@ public class TypeInfo {
|
||||
return jsTypeInfo.hasField(fieldName);
|
||||
}
|
||||
|
||||
if (javaClass == null) return false;
|
||||
try {
|
||||
for (java.lang.reflect.Field f : javaClass.getFields()) {
|
||||
if (f.getName().equals(fieldName)) {
|
||||
return true;
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
for (java.lang.reflect.Field f : javaClass.getFields()) {
|
||||
if (f.getName().equals(fieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
return false;
|
||||
}
|
||||
if (rawType != null && rawType != this) return rawType.hasField(fieldName);
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasEnumConstant(String constantName) {
|
||||
if (javaClass == null || !javaClass.isEnum()) return false;
|
||||
/**
|
||||
* Returns all enum constants for this type as a name-to-EnumConstantInfo map.
|
||||
* For Java enum types, builds the map via reflection. For script-defined enums,
|
||||
* ScriptTypeInfo overrides this to return its stored constants directly.
|
||||
*
|
||||
* @return Map of constant name to EnumConstantInfo (empty if not an enum or no constants)
|
||||
*/
|
||||
public Map<String, EnumConstantInfo> getEnumConstants() {
|
||||
if (javaClass == null || !javaClass.isEnum()) {
|
||||
if (rawType != null && rawType != this) {
|
||||
return rawType.getEnumConstants();
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, EnumConstantInfo> result = new LinkedHashMap<>();
|
||||
try {
|
||||
Object[] constants = javaClass.getEnumConstants();
|
||||
for (Object constant : constants) {
|
||||
if (constant.toString().equals(constantName))
|
||||
return true;
|
||||
String name = constant.toString();
|
||||
result.put(name, EnumConstantInfo.fromReflection(name, this, null));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
// Security or linkage error — return whatever we have so far
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean hasEnumConstant(String constantName) {
|
||||
if (javaClass != null && javaClass.isEnum()) {
|
||||
try {
|
||||
Object[] constants = javaClass.getEnumConstants();
|
||||
for (Object constant : constants) {
|
||||
if (constant.toString().equals(constantName))
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (rawType != null && rawType != this) return rawType.hasEnumConstant(constantName);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -882,16 +1129,19 @@ public class TypeInfo {
|
||||
* Get an enum constant by name.
|
||||
*/
|
||||
public EnumConstantInfo getEnumConstant(String constantName) {
|
||||
if (javaClass == null || !javaClass.isEnum()) return null;
|
||||
try {
|
||||
Object[] constants = javaClass.getEnumConstants();
|
||||
for (Object constant : constants) {
|
||||
if (constant.toString().equals(constantName))
|
||||
return EnumConstantInfo.fromReflection(constantName, this, null);
|
||||
if (javaClass != null && javaClass.isEnum()) {
|
||||
try {
|
||||
Object[] constants = javaClass.getEnumConstants();
|
||||
for (Object constant : constants) {
|
||||
if (constant.toString().equals(constantName))
|
||||
return EnumConstantInfo.fromReflection(constantName, this, null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
return null;
|
||||
}
|
||||
if (rawType != null && rawType != this) return rawType.getEnumConstant(constantName);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -914,6 +1164,13 @@ public class TypeInfo {
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} else if (rawType != null && rawType != this) {
|
||||
MethodInfo rawMethod = rawType.getMethodInfo(methodName);
|
||||
if (rawMethod != null && isParameterized()) {
|
||||
GenericContext ctx = GenericContext.forReceiver(this);
|
||||
return rawMethod.substituteTypeParams(ctx);
|
||||
}
|
||||
return rawMethod;
|
||||
}
|
||||
TypeInfo obj = TypeInfo.object();
|
||||
if (obj != this) return obj.getMethodInfo(methodName);
|
||||
@@ -950,6 +1207,16 @@ public class TypeInfo {
|
||||
// Security or linkage error
|
||||
}
|
||||
if (!overloads.isEmpty()) return overloads;
|
||||
} else if (rawType != null && rawType != this) {
|
||||
java.util.List<MethodInfo> rawOverloads = rawType.getAllMethodOverloads(methodName);
|
||||
if (!rawOverloads.isEmpty() && isParameterized()) {
|
||||
GenericContext ctx = GenericContext.forReceiver(this);
|
||||
for (MethodInfo m : rawOverloads) {
|
||||
overloads.add(m.substituteTypeParams(ctx));
|
||||
}
|
||||
return overloads;
|
||||
}
|
||||
return rawOverloads;
|
||||
}
|
||||
TypeInfo obj = TypeInfo.object();
|
||||
if (obj != this) return obj.getAllMethodOverloads(methodName);
|
||||
@@ -1016,16 +1283,25 @@ public class TypeInfo {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (javaClass == null) return null;
|
||||
try {
|
||||
for (java.lang.reflect.Field f : javaClass.getFields()) {
|
||||
if (f.getName().equals(fieldName)) {
|
||||
// Create a synthetic FieldInfo from reflection
|
||||
return FieldInfo.fromReflection(f, this);
|
||||
if (javaClass != null) {
|
||||
try {
|
||||
for (java.lang.reflect.Field f : javaClass.getFields()) {
|
||||
if (f.getName().equals(fieldName)) {
|
||||
return FieldInfo.fromReflection(f, this);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Security or linkage error
|
||||
return null;
|
||||
}
|
||||
if (rawType != null && rawType != this) {
|
||||
FieldInfo rawField = rawType.getFieldInfo(fieldName);
|
||||
if (rawField != null && isParameterized()) {
|
||||
GenericContext ctx = GenericContext.forReceiver(this);
|
||||
return rawField.substituteTypeParams(ctx);
|
||||
}
|
||||
return rawField;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1144,7 +1420,17 @@ public class TypeInfo {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Not a functional interface
|
||||
// Not a functional interface — try rawType for parameterized script types
|
||||
if (rawType != null && rawType != this) {
|
||||
MethodInfo rawSAM = rawType.getSingleAbstractMethod();
|
||||
if (rawSAM != null && isParameterized()) {
|
||||
GenericContext ctx = GenericContext.forReceiver(this);
|
||||
cachedSAM = rawSAM.substituteTypeParams(ctx);
|
||||
return cachedSAM;
|
||||
}
|
||||
cachedSAM = rawSAM;
|
||||
return cachedSAM;
|
||||
}
|
||||
cachedSAM = null;
|
||||
return null;
|
||||
|
||||
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
package noppes.npcs.client.gui.util.script.interpreter.type;
|
||||
|
||||
import noppes.npcs.client.gui.util.script.interpreter.js_parser.TypeParamInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility for substituting type variables (like T, E, K, V) with concrete types.
|
||||
*
|
||||
* Used when resolving members on parameterized types:
|
||||
* - List<String>.get(int) returns String (not T/Object)
|
||||
* - Map<String, Integer>.get(Object) returns Integer (not V)
|
||||
*
|
||||
* Handles nested generics:
|
||||
* - List<Map<String, Integer>>.get(0) returns Map<String, Integer>
|
||||
*/
|
||||
public class TypeSubstitutor {
|
||||
|
||||
/**
|
||||
* Create a binding map from declared type parameters to applied type arguments.
|
||||
*
|
||||
* Example: For List<String> where List is declared as interface List<E>:
|
||||
* - declaredParams = [TypeParamInfo("E", ...)]
|
||||
* - appliedArgs = [TypeInfo(String)]
|
||||
* - Result: {"E" -> TypeInfo(String)}
|
||||
*
|
||||
* @param declaredParams The declared type parameters (from the generic type definition)
|
||||
* @param appliedArgs The applied type arguments (from the parameterized usage)
|
||||
* @return A map from type parameter names to their concrete TypeInfo values
|
||||
*/
|
||||
public static Map<String, TypeInfo> createBindings(List<TypeParamInfo> declaredParams, List<TypeInfo> appliedArgs) {
|
||||
Map<String, TypeInfo> bindings = new HashMap<>();
|
||||
|
||||
if (declaredParams == null || appliedArgs == null) {
|
||||
return bindings;
|
||||
}
|
||||
|
||||
int count = Math.min(declaredParams.size(), appliedArgs.size());
|
||||
for (int i = 0; i < count; i++) {
|
||||
String paramName = declaredParams.get(i).getName();
|
||||
TypeInfo argType = appliedArgs.get(i);
|
||||
if (paramName != null && argType != null) {
|
||||
bindings.put(paramName, argType);
|
||||
}
|
||||
}
|
||||
|
||||
return bindings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create bindings from a parameterized receiver type.
|
||||
*
|
||||
* @param receiverType The parameterized type (e.g., List<String>)
|
||||
* @return Bindings from the type's declared params to its applied args
|
||||
*/
|
||||
public static Map<String, TypeInfo> createBindingsFromReceiver(TypeInfo receiverType) {
|
||||
if (receiverType == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
// Get the raw type to access declared type parameters
|
||||
TypeInfo rawType = receiverType.getRawType();
|
||||
List<TypeParamInfo> declaredParams = null;
|
||||
|
||||
// Get declared params from JSTypeInfo or reflection
|
||||
if (rawType.isJSType() && rawType.getJSTypeInfo() != null) {
|
||||
declaredParams = rawType.getJSTypeInfo().getTypeParams();
|
||||
} else {
|
||||
declaredParams = rawType.getTypeParams();
|
||||
}
|
||||
|
||||
// Get applied args from the parameterized type
|
||||
List<TypeInfo> appliedArgs = receiverType.getAppliedTypeArgs();
|
||||
|
||||
return createBindings(declaredParams, appliedArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitute type variables in a type using the given bindings.
|
||||
*
|
||||
* @param type The type to substitute (may contain type variables like T, E)
|
||||
* @param bindings The map from type variable names to concrete types
|
||||
* @return The substituted type
|
||||
*/
|
||||
public static TypeInfo substitute(TypeInfo type, Map<String, TypeInfo> bindings) {
|
||||
if (type == null || bindings == null || bindings.isEmpty()) {
|
||||
return type;
|
||||
}
|
||||
|
||||
String typeName = type.getSimpleName();
|
||||
|
||||
// Check if this type is itself a type variable.
|
||||
// Two cases:
|
||||
// 1. Unresolved TypeInfo (Java reflection path: TypeInfo.fromGenericType creates these for T, E, K, V)
|
||||
// 2. TypeInfo.typeParameter() (script-defined path: resolved but tagged as a type parameter)
|
||||
// For tagged type parameters, use getTypeParameterName() as the lookup key.
|
||||
if (!type.isResolved() || type.isTypeParameter()) {
|
||||
String lookupKey = type.isTypeParameter() ? type.getTypeParameterName() : typeName;
|
||||
TypeInfo substitution = bindings.get(lookupKey);
|
||||
if (substitution != null) {
|
||||
return substitution;
|
||||
}
|
||||
}
|
||||
|
||||
// If the type has applied type arguments, substitute within them recursively
|
||||
if (type.isParameterized()) {
|
||||
List<TypeInfo> originalArgs = type.getAppliedTypeArgs();
|
||||
List<TypeInfo> substitutedArgs = new ArrayList<>();
|
||||
boolean changed = false;
|
||||
|
||||
for (TypeInfo arg : originalArgs) {
|
||||
TypeInfo substitutedArg = substitute(arg, bindings);
|
||||
substitutedArgs.add(substitutedArg);
|
||||
if (substitutedArg != arg) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
// Create a new parameterized type with substituted args
|
||||
return type.getRawType().parameterize(substitutedArgs);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array types - substitute the element type
|
||||
if (type.getSimpleName().endsWith("[]")) {
|
||||
TypeStringNormalizer.ArraySplit split = TypeStringNormalizer.splitArraySuffixes(typeName);
|
||||
String elementTypeName = split.base;
|
||||
int dims = split.dimensions;
|
||||
TypeInfo elementSubstitution = elementTypeName != null ? bindings.get(elementTypeName) : null;
|
||||
if (elementSubstitution != null) {
|
||||
TypeInfo result = elementSubstitution;
|
||||
for (int i = 0; i < dims; i++) {
|
||||
result = TypeInfo.arrayOf(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package noppes.npcs.config;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.ScaledResolution;
|
||||
import net.minecraft.client.renderer.GLAllocation;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
|
||||
import java.awt.AlphaComposite;
|
||||
import java.awt.Color;
|
||||
@@ -43,13 +45,13 @@ public class GlyphCache {
|
||||
* The width in pixels of every texture used for caching pre-rendered glyph images. Used by GlyphCache when calculating
|
||||
* floating point 0.0-1.0 texture coordinates. Must be a power of two for mip-mapping to work.
|
||||
*/
|
||||
private static final int TEXTURE_WIDTH = 256;
|
||||
private static final int TEXTURE_WIDTH = 512;
|
||||
|
||||
/**
|
||||
* The height in pixels of every texture used for caching pre-rendered glyph images. Used by GlyphCache when calculating
|
||||
* floating point 0.0-1.0 texture coordinates. Must be a power of two for mip-mapping to work.
|
||||
*/
|
||||
private static final int TEXTURE_HEIGHT = 256;
|
||||
private static final int TEXTURE_HEIGHT = 512;
|
||||
|
||||
/**
|
||||
* Initial width in pixels of the stringImage buffer used to extract individual glyph images.
|
||||
@@ -66,7 +68,23 @@ public class GlyphCache {
|
||||
* glyphs from "bleeding through" when the scaled GUI resolution is not pixel aligned and sometimes results in off-by-one
|
||||
* sampling of the glyph cache textures.
|
||||
*/
|
||||
private static final int GLYPH_BORDER = 1;
|
||||
private static final int GLYPH_BORDER = 2;
|
||||
|
||||
/**
|
||||
* Extra padding (in pixels) added around each glyph's reported pixel bounds before blitting into the atlas.
|
||||
* Font hinting (especially GASP-aware rendering) can cause rendered pixels to extend beyond the bounds
|
||||
* reported by GlyphVector.getGlyphPixelBounds(). This padding ensures those overflow pixels are captured.
|
||||
*/
|
||||
private static final int GLYPH_PADDING = 2;
|
||||
|
||||
/**
|
||||
* Minimum spacing (in pixels) inserted between adjacent glyphs in the temporary string image during
|
||||
* rasterization. Each glyph is shifted right by glyphSpacing * glyphIndex to prevent kerning from
|
||||
* merging adjacent glyphs. This base value is used for plain/bold styles. For italic fonts, the
|
||||
* spacing is increased dynamically by {@link #computeGlyphSpacing(Font)} to account for the
|
||||
* horizontal overhang caused by the italic slant.
|
||||
*/
|
||||
private static final int GLYPH_SPACING_BASE = 2 * GLYPH_PADDING + 1;
|
||||
|
||||
/**
|
||||
* Transparent (alpha zero) white background color for use with BufferedImage.clearRect().
|
||||
@@ -83,6 +101,21 @@ public class GlyphCache {
|
||||
*/
|
||||
private boolean antiAliasEnabled = false;
|
||||
|
||||
/**
|
||||
* The current Minecraft GUI scale factor (1-4). Glyphs are rasterized at fontSize * guiScaleFactor
|
||||
* so the atlas has enough texel density for the actual display resolution. This prevents
|
||||
* GL_LINEAR texture filtering from diluting alpha values at high resolutions.
|
||||
*/
|
||||
private int guiScaleFactor = 1;
|
||||
|
||||
/**
|
||||
* Return the GUI scale factor used for glyph rasterization. StringCache uses this
|
||||
* to compute an effective render scale that compensates for the larger atlas images.
|
||||
*/
|
||||
int getGuiScaleFactor() {
|
||||
return guiScaleFactor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Temporary image for rendering a string to and then extracting the glyph images from.
|
||||
@@ -95,6 +128,7 @@ public class GlyphCache {
|
||||
private Graphics2D stringGraphics;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* All font glyphs are packed inside this image and are then loaded from here into an OpenGL texture.
|
||||
*/
|
||||
@@ -107,9 +141,53 @@ public class GlyphCache {
|
||||
|
||||
/**
|
||||
* Needed for all text layout operations that create GlyphVectors (maps point size to pixel size).
|
||||
* Rebuilt after setRenderingHints() so layout FRC matches rendering FRC (especially for GASP hinting).
|
||||
*/
|
||||
private FontRenderContext fontRenderContext = glyphCacheGraphics.getFontRenderContext();
|
||||
|
||||
/**
|
||||
* Return the current FontRenderContext used for glyph layout and rendering.
|
||||
* StringCache must use this same FRC when computing glyph pixel bounds for positioning,
|
||||
* so that layout positions match the GASP-aware hinting used during rasterization.
|
||||
*/
|
||||
FontRenderContext getFontRenderContext() {
|
||||
return fontRenderContext;
|
||||
}
|
||||
|
||||
int getGlyphPadding() {
|
||||
return GLYPH_PADDING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the inter-glyph spacing needed for a given font when rasterizing into the temporary
|
||||
* string image. For plain and bold fonts, the base spacing (2*GLYPH_PADDING+1) is sufficient.
|
||||
* For italic fonts, the slant causes the top of each glyph to lean horizontally, and at large
|
||||
* raster sizes (e.g. 54pt at scale 3) this overhang can be 8+ pixels — far exceeding the base
|
||||
* spacing's 1-pixel effective gap. Without adequate spacing, adjacent italic glyphs' rendered
|
||||
* pixels overlap in the string image, and narrow glyphs like 'I'/'i' capture neighboring
|
||||
* fragments as visible dot artifacts in their cached texture.
|
||||
*
|
||||
* The italic overhang is computed as: lineHeight * |italicAngle|. Java2D's standard italic
|
||||
* angle is ~0.2 (approx 12°). A safety factor of 1.5× is applied since antialiasing can
|
||||
* produce faint alpha slightly beyond the geometric slant boundary.
|
||||
*/
|
||||
private int computeGlyphSpacing(Font font) {
|
||||
float italicAngle = font.getItalicAngle();
|
||||
if (italicAngle == 0 && font.isItalic()) {
|
||||
italicAngle = -0.2f; // Java2D synthetic italic default shear
|
||||
}
|
||||
|
||||
if (italicAngle == 0) {
|
||||
return GLYPH_SPACING_BASE;
|
||||
}
|
||||
|
||||
java.awt.font.LineMetrics lm = font.getLineMetrics("I", fontRenderContext);
|
||||
float lineHeight = lm.getAscent() + lm.getDescent();
|
||||
int italicOverhang = (int) Math.ceil(lineHeight * Math.abs(italicAngle) * 1.5f);
|
||||
|
||||
return Math.max(GLYPH_SPACING_BASE, 2 * GLYPH_PADDING + italicOverhang + 1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Intermediate data array for use with textureImage.getRgb().
|
||||
@@ -258,6 +336,7 @@ public class GlyphCache {
|
||||
|
||||
fontSize = size;
|
||||
antiAliasEnabled = antiAlias;
|
||||
guiScaleFactor = queryGuiScaleFactor();
|
||||
setRenderingHints();
|
||||
}
|
||||
|
||||
@@ -273,15 +352,60 @@ public class GlyphCache {
|
||||
|
||||
fontSize = size;
|
||||
antiAliasEnabled = antiAlias;
|
||||
guiScaleFactor = queryGuiScaleFactor();
|
||||
setRenderingHints();
|
||||
}
|
||||
}
|
||||
|
||||
private static int queryGuiScaleFactor() {
|
||||
try {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
if (mc != null && mc.displayWidth > 0 && mc.displayHeight > 0) {
|
||||
ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
|
||||
return Math.max(1, sr.getScaleFactor());
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the GUI scale factor has changed (e.g. due to window resize) and invalidate the glyph
|
||||
* cache if so. Glyphs must be re-rasterized at the new density to prevent GL_LINEAR filtering
|
||||
* from diluting alpha at high display resolutions.
|
||||
*
|
||||
* @return true if the scale factor changed and glyphs were invalidated
|
||||
*/
|
||||
boolean checkAndUpdateScaleFactor() {
|
||||
int newScale = queryGuiScaleFactor();
|
||||
if (newScale != guiScaleFactor) {
|
||||
guiScaleFactor = newScale;
|
||||
invalidateGlyphCache();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void invalidateGlyphCache() {
|
||||
glyphCache.clear();
|
||||
fontCache.clear();
|
||||
cachePosX = GLYPH_BORDER;
|
||||
cachePosY = GLYPH_BORDER;
|
||||
cacheLineHeight = 0;
|
||||
allocateGlyphCacheTexture();
|
||||
allocateStringImage(STRING_WIDTH, STRING_HEIGHT);
|
||||
}
|
||||
|
||||
int fontHeight(String s) {
|
||||
Font font = lookupFont(s.toCharArray(), 0, s.length(), 0);
|
||||
return (int) (font.getLineMetrics(s, this.fontRenderContext).getHeight());
|
||||
}
|
||||
|
||||
java.awt.font.LineMetrics getLineMetrics(String s) {
|
||||
Font font = lookupFont(s.toCharArray(), 0, s.length(), 0);
|
||||
return font.getLineMetrics(s, this.fontRenderContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a single OpenType font, perform full text layout and create a new GlyphVector for a string.
|
||||
*
|
||||
@@ -311,6 +435,9 @@ public class GlyphCache {
|
||||
* @return an OpenType font capable of displaying at least the first character at the start position in text
|
||||
*/
|
||||
Font lookupFont(char text[], int start, int limit, int style) {
|
||||
/* Rasterize at scaled size so atlas texel density matches display resolution */
|
||||
int renderSize = fontSize * guiScaleFactor;
|
||||
|
||||
/* Try using an already known base font; the first font in usedFonts list is the one set with setDefaultFont() */
|
||||
Iterator<Font> iterator = usedFonts.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
@@ -318,7 +445,7 @@ public class GlyphCache {
|
||||
Font font = iterator.next();
|
||||
if (font.canDisplayUpTo(text, start, limit) != start) {
|
||||
/* Return a font instance of the proper point size and style; usedFonts has only 1pt sized plain style fonts */
|
||||
return font.deriveFont(style, fontSize);
|
||||
return font.deriveFont(style, renderSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +460,7 @@ public class GlyphCache {
|
||||
usedFonts.add(font);
|
||||
|
||||
/* Return a font instance of the proper point size and style; allFonts has only 1pt sized plain style fonts */
|
||||
return font.deriveFont(style, fontSize);
|
||||
return font.deriveFont(style, renderSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +468,7 @@ public class GlyphCache {
|
||||
Font font = usedFonts.get(0);
|
||||
|
||||
/* Return a font instance of the proper point size and style; usedFonts only 1pt sized plain style fonts */
|
||||
return font.deriveFont(style, fontSize);
|
||||
return font.deriveFont(style, renderSize);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -384,14 +511,37 @@ public class GlyphCache {
|
||||
int numGlyphs = vector.getNumGlyphs(); /* Length of the GlyphVector */
|
||||
Rectangle dirty = null; /* Total area within texture that needs to be updated with glTexSubImage2D() */
|
||||
boolean vectorRendered = false; /* True if entire GlyphVector was rendered into stringImage */
|
||||
int glyphSpacing = computeGlyphSpacing(font);
|
||||
|
||||
for (int index = 0; index < numGlyphs; index++) {
|
||||
int charIndex = start + vector.getGlyphCharIndex(index);
|
||||
if (charIndex >= start && charIndex < limit) {
|
||||
char c = text[charIndex];
|
||||
if (c == '\n' || c == '\r') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* If this glyph code is already in glyphCache, then there is no reason to pre-render it again */
|
||||
int glyphCode = vector.getGlyphCode(index);
|
||||
if (glyphCache.containsKey(fontKey | glyphCode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip whitespace glyphs that have no visible ink (spaces, tabs, etc.). These characters
|
||||
* contribute only advance width, not visible pixels. Without this check, GLYPH_PADDING
|
||||
* expansion would create a small non-zero rect for the empty glyph, and the blit from
|
||||
* stringImage could capture stray antialiased fringe pixels from neighboring glyphs,
|
||||
* causing whitespace to render as visible dots.
|
||||
*/
|
||||
{
|
||||
Rectangle glyphBounds = vector.getGlyphPixelBounds(index, fontRenderContext, 0, 0);
|
||||
if (glyphBounds.width == 0 || glyphBounds.height == 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The only way to get glyph shapes with font hinting is to draw the entire glyph vector into a
|
||||
* temporary BufferedImage, and then bit blit the individual glyphs based on their bounding boxes
|
||||
@@ -410,7 +560,7 @@ public class GlyphCache {
|
||||
*/
|
||||
for (int i = 0; i < numGlyphs; i++) {
|
||||
Point2D pos = vector.getGlyphPosition(i);
|
||||
pos.setLocation(pos.getX() + 2 * i, pos.getY());
|
||||
pos.setLocation(pos.getX() + glyphSpacing * i, pos.getY());
|
||||
vector.setGlyphPosition(i, pos);
|
||||
}
|
||||
|
||||
@@ -422,15 +572,26 @@ public class GlyphCache {
|
||||
*/
|
||||
vectorBounds = vector.getPixelBounds(fontRenderContext, 0, 0);
|
||||
|
||||
/* Enlage the stringImage if it is too small to store the entire rendered string */
|
||||
/* Expand bounds to account for hinting overflow */
|
||||
vectorBounds.grow(GLYPH_PADDING, GLYPH_PADDING);
|
||||
|
||||
/* Enlarge the stringImage if it is too small to store the entire rendered string */
|
||||
if (stringImage == null || vectorBounds.width > stringImage.getWidth() || vectorBounds.height > stringImage.getHeight()) {
|
||||
int width = Math.max(vectorBounds.width, stringImage.getWidth());
|
||||
int height = Math.max(vectorBounds.height, stringImage.getHeight());
|
||||
allocateStringImage(width, height);
|
||||
}
|
||||
|
||||
/* Erase the upper-left corner where the string will get drawn*/
|
||||
stringGraphics.clearRect(0, 0, vectorBounds.width, vectorBounds.height);
|
||||
/*
|
||||
* Clear the ENTIRE stringImage, not just the vectorBounds area. The stringImage is reused
|
||||
* across calls and may be larger than the current vectorBounds. Individual glyph pixel bounds
|
||||
* (from getGlyphPixelBounds()) can differ slightly from the aggregate vector bounds
|
||||
* (from getPixelBounds()) due to integer rounding. After GLYPH_PADDING expansion, a glyph's
|
||||
* padded rect could extend 1-2 pixels beyond the vectorBounds area, capturing stale pixels
|
||||
* from a previous render. This caused tiny dot artifacts above capital letters whose ink
|
||||
* extends to the top of the line bounds. Clearing the full image eliminates this class of bugs.
|
||||
*/
|
||||
stringGraphics.clearRect(0, 0, stringImage.getWidth(), stringImage.getHeight());
|
||||
|
||||
/* Draw string with opaque white color and baseline adjustment so the upper-left corner of the image is at (0,0) */
|
||||
stringGraphics.drawGlyphVector(vector, -vectorBounds.x, -vectorBounds.y);
|
||||
@@ -441,8 +602,35 @@ public class GlyphCache {
|
||||
* by this method is positioned around the origin of each individual glyph." However, the actual
|
||||
* bounds are all relative to the start of the entire GlyphVector, which is actually more useful
|
||||
* for extracting the glyph's image from the rendered string.
|
||||
*
|
||||
* Passing fontRenderContext ensures bounds match the GASP-aware rendering hints.
|
||||
*/
|
||||
Rectangle rect = vector.getGlyphPixelBounds(index, null, -vectorBounds.x, -vectorBounds.y);
|
||||
Rectangle rect = vector.getGlyphPixelBounds(index, fontRenderContext, -vectorBounds.x, -vectorBounds.y);
|
||||
rect.grow(GLYPH_PADDING, GLYPH_PADDING);
|
||||
|
||||
/*
|
||||
* Clamp the padded rect to the stringImage bounds. Due to rounding differences between
|
||||
* getPixelBounds() and getGlyphPixelBounds(), a padded glyph rect can occasionally extend
|
||||
* beyond the image edge. Reading pixels outside the image would cause an exception or
|
||||
* capture garbage data.
|
||||
*/
|
||||
if (rect.x < 0) {
|
||||
rect.width += rect.x; // shrink width by the overshoot
|
||||
rect.x = 0;
|
||||
}
|
||||
if (rect.y < 0) {
|
||||
rect.height += rect.y; // shrink height by the overshoot
|
||||
rect.y = 0;
|
||||
}
|
||||
if (rect.x + rect.width > stringImage.getWidth()) {
|
||||
rect.width = stringImage.getWidth() - rect.x;
|
||||
}
|
||||
if (rect.y + rect.height > stringImage.getHeight()) {
|
||||
rect.height = stringImage.getHeight() - rect.y;
|
||||
}
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* If the current line in cache image is full, then advance to the next line */
|
||||
if (cachePosX + rect.width + GLYPH_BORDER > TEXTURE_WIDTH) {
|
||||
@@ -573,10 +761,23 @@ public class GlyphCache {
|
||||
private void setRenderingHints() {
|
||||
stringGraphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
|
||||
antiAliasEnabled ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
|
||||
stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
|
||||
antiAliasEnabled ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
|
||||
|
||||
// if (antiAliasEnabled) {
|
||||
// stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
|
||||
// RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
// stringGraphics.setRenderingHint(RenderingHints.KEY_RENDERING,
|
||||
// RenderingHints.VALUE_RENDER_QUALITY);
|
||||
// } else {
|
||||
// stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
|
||||
// RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
|
||||
// }
|
||||
|
||||
stringGraphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
|
||||
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
stringGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
stringGraphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
|
||||
|
||||
fontRenderContext = stringGraphics.getFontRenderContext();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -608,8 +809,10 @@ public class GlyphCache {
|
||||
GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, imageBuffer);
|
||||
|
||||
/* Explicitely disable mipmap support becuase updateTexture() will only update the base level 0 */
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,11 +3,13 @@ package noppes.npcs.config;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import noppes.npcs.LogWriter;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.Point;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.font.GlyphVector;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.text.Bidi;
|
||||
import java.util.ArrayList;
|
||||
@@ -71,6 +73,38 @@ public class StringCache {
|
||||
*/
|
||||
private static final int STRIKETHROUGH_THICKNESS = 2;
|
||||
|
||||
// BetterFonts base scale: maps internal 2x glyph coords to GUI coords.
|
||||
// With scale-aware rasterization, the effective scale becomes BASE_RENDER_SCALE / guiScaleFactor.
|
||||
private static final float BASE_RENDER_SCALE = 0.5F;
|
||||
private static final boolean FONT_DEBUG_LOG = Boolean.getBoolean("cnpc.font.debug");
|
||||
private static final boolean FONT_DEBUG_DISABLE_INK_OFFSET = Boolean.getBoolean("cnpc.font.debug.disableInkOffset");
|
||||
|
||||
private static boolean isLineBreak(char c) {
|
||||
return c == '\n' || c == '\r';
|
||||
}
|
||||
|
||||
private static boolean isSpacingDebugGlyph(char c) {
|
||||
return c == '(' || c == '"' || c == 't' || c == '.';
|
||||
}
|
||||
|
||||
private static String debugGlyphLabel(char c) {
|
||||
return c == '"' ? "\\\"" : String.valueOf(c);
|
||||
}
|
||||
|
||||
private float renderScale() {
|
||||
return BASE_RENDER_SCALE / glyphCache.getGuiScaleFactor();
|
||||
}
|
||||
|
||||
private float pixelSnap(float coord) {
|
||||
float scale = renderScale();
|
||||
float invScale = 1.0F / scale;
|
||||
return (float) Math.floor(coord * invScale + 0.5F) * scale;
|
||||
}
|
||||
|
||||
private int baselineOffset = BASELINE_OFFSET;
|
||||
private int underlineOffset = UNDERLINE_OFFSET;
|
||||
private int strikethroughOffset = STRIKETHROUGH_OFFSET;
|
||||
|
||||
/**
|
||||
* Reference to the unicode.FontRenderer class. Needed for creating GlyphVectors and retrieving glyph texture coordinates.
|
||||
*/
|
||||
@@ -319,31 +353,21 @@ public class StringCache {
|
||||
* the string to which this Glyph object belongs.
|
||||
*/
|
||||
static private class Glyph implements Comparable<Glyph> {
|
||||
/**
|
||||
* The index into the original string (i.e. with color codes) for the character that generated this glyph.
|
||||
*/
|
||||
public int stringIndex;
|
||||
|
||||
/**
|
||||
* Texture ID and position/size of the glyph's pre-rendered image within the cache texture.
|
||||
*/
|
||||
public GlyphCache.Entry texture;
|
||||
|
||||
/**
|
||||
* Glyph's horizontal position (in pixels) relative to the entire string's baseline
|
||||
*/
|
||||
/** Logical horizontal position from getGlyphPosition() — used for spacing/advance */
|
||||
public int x;
|
||||
|
||||
/**
|
||||
* Glyph's vertical position (in pixels) relative to the entire string's baseline
|
||||
*/
|
||||
/** Ink vertical position from getGlyphPixelBounds() — used for texture quad placement */
|
||||
public int y;
|
||||
|
||||
/**
|
||||
* Glyph's horizontal advance (in pixels) used for strikethrough and underline effects
|
||||
*/
|
||||
/** Horizontal advance (logical) used for strikethrough and underline effects */
|
||||
public int advance;
|
||||
|
||||
/** Ink X offset from logical position (inkBoundsX - logicalX) for texture quad placement */
|
||||
public int inkOffsetX;
|
||||
|
||||
/**
|
||||
* Allows arrays of Glyph objects to be sorted. Performs numeric comparison on stringIndex.
|
||||
*
|
||||
@@ -451,7 +475,44 @@ public class StringCache {
|
||||
if (g.y > maxY)
|
||||
maxY = g.y;
|
||||
}
|
||||
fontHeight = (int) ((maxY + height) / 2.0F - minY / 2.0F);
|
||||
int scaleFactor = glyphCache.getGuiScaleFactor();
|
||||
fontHeight = (int) (((maxY + height) - minY) / 2.0F / scaleFactor);
|
||||
updateFontMetrics();
|
||||
}
|
||||
|
||||
private void updateFontMetrics() {
|
||||
java.awt.font.LineMetrics lm = glyphCache.getLineMetrics("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
|
||||
float ascent = lm.getAscent();
|
||||
float descent = lm.getDescent();
|
||||
|
||||
/*
|
||||
* baselineOffset is added directly to startY (GUI pixels) at render time, so it must be a
|
||||
* scale-invariant GUI-pixel value. The constant BASELINE_OFFSET (7) is correct for the default
|
||||
* 18pt font and remains stable across all GUI scale factors because it operates in GUI coordinate
|
||||
* space — independent of the glyph rasterization scale.
|
||||
*
|
||||
* A dynamic formula like (ascent - padding) * renderScale is fragile: ascent comes from the
|
||||
* scaled font (fontSize * scaleFactor) and rounding/font-metric variations across scales can
|
||||
* produce values 1-2 pixels too large, visibly shifting text down within its highlight box.
|
||||
*/
|
||||
baselineOffset = BASELINE_OFFSET;
|
||||
|
||||
/*
|
||||
* underlineOffset and strikethroughOffset are multiplied by renderScale() at draw time
|
||||
* (same as glyph.x/glyph.y), so they must be in glyph-space pixels. These scale correctly
|
||||
* because ascent/descent are proportional to the rasterization size.
|
||||
*/
|
||||
underlineOffset = (int) (descent * 0.25f + 0.5f);
|
||||
strikethroughOffset = -(int) (ascent * 0.35f + 0.5f);
|
||||
}
|
||||
|
||||
private void checkForScaleChange() {
|
||||
if (glyphCache.checkAndUpdateScaleFactor()) {
|
||||
weakRefCache.clear();
|
||||
stringCache.clear();
|
||||
cacheDightGlyphs();
|
||||
updateHeight();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -490,12 +551,14 @@ public class StringCache {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Detect window resize: if GUI scale factor changed, invalidate all caches and re-rasterize */
|
||||
checkForScaleChange();
|
||||
|
||||
/* Make sure the entire string is cached before rendering and return its glyph representation */
|
||||
Entry entry = cacheString(str);
|
||||
|
||||
/* Adjust the baseline of the string because the startY coordinate in Minecraft is for the top of the string */
|
||||
startY += BASELINE_OFFSET;
|
||||
startY += baselineOffset;
|
||||
|
||||
/* Color currently selected by color code; reapplied to Tessellator instance after glBindTexture() */
|
||||
|
||||
@@ -548,7 +611,13 @@ public class StringCache {
|
||||
/* Select the current glyph's texture information and horizontal layout position within this string */
|
||||
Glyph glyph = entry.glyphs[glyphIndex];
|
||||
GlyphCache.Entry texture = glyph.texture;
|
||||
int glyphX = glyph.x;
|
||||
|
||||
/* Skip glyphs with no cached texture (whitespace characters like spaces and tabs) */
|
||||
if (texture == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int glyphX = glyph.x + glyph.inkOffsetX;
|
||||
|
||||
/*
|
||||
* Replace ASCII digits in the string with their respective glyphs; strings differing by digits are only cached once.
|
||||
@@ -557,6 +626,10 @@ public class StringCache {
|
||||
* of the width mismatch.
|
||||
*/
|
||||
char c = str.charAt(glyph.stringIndex);
|
||||
if (isLineBreak(c)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c >= '0' && c <= '9') {
|
||||
int oldWidth = texture.width;
|
||||
texture = digitGlyphs[fontStyle][c - '0'].texture;
|
||||
@@ -580,10 +653,21 @@ public class StringCache {
|
||||
}
|
||||
|
||||
/* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */
|
||||
float x1 = startX + (glyphX) / 2.0F;
|
||||
float x2 = startX + (glyphX + texture.width) / 2.0F;
|
||||
float y1 = startY + (glyph.y) / 2.0F;
|
||||
float y2 = startY + (glyph.y + texture.height) / 2.0F;
|
||||
if (FONT_DEBUG_DISABLE_INK_OFFSET) {
|
||||
glyphX = glyph.x;
|
||||
}
|
||||
float rs = renderScale();
|
||||
float x1 = pixelSnap(startX + (glyphX) * rs);
|
||||
float x2 = pixelSnap(startX + (glyphX + texture.width) * rs);
|
||||
float y1 = pixelSnap(startY + (glyph.y) * rs);
|
||||
float y2 = pixelSnap(startY + (glyph.y + texture.height) * rs);
|
||||
|
||||
if (FONT_DEBUG_LOG && isSpacingDebugGlyph(c)) {
|
||||
LogWriter.info("[StringCache glyph-debug] char='" + debugGlyphLabel(c) + "' idx=" + glyph.stringIndex +
|
||||
" x=" + glyph.x + " advance=" + glyph.advance + " texW=" + texture.width +
|
||||
" inkOffsetX=" + glyph.inkOffsetX + " quadX1=" + x1 + " quadX2=" + x2 +
|
||||
" y=" + glyph.y + " tex=" + texture.textureName);
|
||||
}
|
||||
|
||||
tessellator.addVertexWithUV(x1, y1, 0, texture.u1, texture.v1);
|
||||
tessellator.addVertexWithUV(x1, y2, 0, texture.u1, texture.v2);
|
||||
@@ -620,15 +704,16 @@ public class StringCache {
|
||||
Glyph glyph = entry.glyphs[glyphIndex];
|
||||
|
||||
/* The strike/underlines are drawn beyond the glyph's width to include the extra space between glyphs */
|
||||
int glyphSpace = glyph.advance - glyph.texture.width;
|
||||
int glyphSpace = glyph.texture != null ? glyph.advance - glyph.texture.width : 0;
|
||||
|
||||
/* Draw underline under glyph if the style is enabled */
|
||||
if ((renderStyle & ColorCode.UNDERLINE) != 0) {
|
||||
/* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */
|
||||
float x1 = startX + (glyph.x - glyphSpace) / 2.0F;
|
||||
float x2 = startX + (glyph.x + glyph.advance) / 2.0F;
|
||||
float y1 = startY + (UNDERLINE_OFFSET) / 2.0F;
|
||||
float y2 = startY + (UNDERLINE_OFFSET + UNDERLINE_THICKNESS) / 2.0F;
|
||||
float urs = renderScale();
|
||||
float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * urs);
|
||||
float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * urs);
|
||||
float y1 = pixelSnap(startY + (underlineOffset) * urs);
|
||||
float y2 = pixelSnap(startY + (underlineOffset + UNDERLINE_THICKNESS) * urs);
|
||||
|
||||
tessellator.addVertex(x1, y1, 0);
|
||||
tessellator.addVertex(x1, y2, 0);
|
||||
@@ -639,10 +724,11 @@ public class StringCache {
|
||||
/* Draw strikethrough in the middle of glyph if the style is enabled */
|
||||
if ((renderStyle & ColorCode.STRIKETHROUGH) != 0) {
|
||||
/* The divide by 2.0F is needed to align with the scaled GUI coordinate system; startX/startY are already scaled */
|
||||
float x1 = startX + (glyph.x - glyphSpace) / 2.0F;
|
||||
float x2 = startX + (glyph.x + glyph.advance) / 2.0F;
|
||||
float y1 = startY + (STRIKETHROUGH_OFFSET) / 2.0F;
|
||||
float y2 = startY + (STRIKETHROUGH_OFFSET + STRIKETHROUGH_THICKNESS) / 2.0F;
|
||||
float srs = renderScale();
|
||||
float x1 = pixelSnap(startX + (glyph.x - glyphSpace) * srs);
|
||||
float x2 = pixelSnap(startX + (glyph.x + glyph.advance) * srs);
|
||||
float y1 = pixelSnap(startY + (strikethroughOffset) * srs);
|
||||
float y2 = pixelSnap(startY + (strikethroughOffset + STRIKETHROUGH_THICKNESS) * srs);
|
||||
|
||||
tessellator.addVertex(x1, y1, 0);
|
||||
tessellator.addVertex(x1, y2, 0);
|
||||
@@ -658,7 +744,7 @@ public class StringCache {
|
||||
|
||||
|
||||
/* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */
|
||||
return entry.advance / 2;
|
||||
return (int) (entry.advance * renderScale());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -673,11 +759,50 @@ public class StringCache {
|
||||
return 0;
|
||||
}
|
||||
|
||||
checkForScaleChange();
|
||||
|
||||
/* Make sure the entire string is cached and rendered since it will probably be used again in a renderString() call */
|
||||
Entry entry = cacheString(str);
|
||||
|
||||
/* Return total horizontal advance (slightly wider than the bounding box, but close enough for centering strings) */
|
||||
return entry.advance / 2;
|
||||
return (int) (entry.advance * renderScale());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the width of a string in pixels when rendered with the specified font style.
|
||||
* This is needed when measuring text that will be rendered with bold/italic formatting,
|
||||
* since bold glyphs are wider than plain glyphs.
|
||||
*
|
||||
* <p>The font style is applied by prepending the appropriate Minecraft formatting codes
|
||||
* (§l for bold, §o for italic) to the string before measuring. This causes cacheString()
|
||||
* to lay out the entire string using the specified font style.
|
||||
*
|
||||
* @param str compute the width of this string
|
||||
* @param fontStyle combination of {@link java.awt.Font#BOLD} and {@link java.awt.Font#ITALIC}
|
||||
* @return the width in pixels (in the scaled GUI coordinate system)
|
||||
*/
|
||||
public int getStringWidth(String str, int fontStyle) {
|
||||
/* Check for invalid arguments */
|
||||
if (str == null || str.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* If plain style, delegate to the normal method (no prefix needed) */
|
||||
if (fontStyle == Font.PLAIN) {
|
||||
return getStringWidth(str);
|
||||
}
|
||||
|
||||
/* Build the style prefix: §l for bold, §o for italic */
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if ((fontStyle & Font.BOLD) != 0) {
|
||||
sb.append('\u00A7').append('l');
|
||||
}
|
||||
if ((fontStyle & Font.ITALIC) != 0) {
|
||||
sb.append('\u00A7').append('o');
|
||||
}
|
||||
sb.append(str);
|
||||
|
||||
return getStringWidth(sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -696,8 +821,8 @@ public class StringCache {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Convert the width from GUI coordinate system to pixels */
|
||||
width += width;
|
||||
/* Convert the width from GUI coordinate system to internal glyph pixels */
|
||||
width = (int) (width / renderScale());
|
||||
|
||||
/* The glyph array for a string is sorted by the string's logical character position */
|
||||
Glyph glyphs[] = cacheString(str).glyphs;
|
||||
@@ -1125,6 +1250,11 @@ public class StringCache {
|
||||
|
||||
/* Break the string up into segments, where each segment can be displayed using a single font */
|
||||
while (start < limit) {
|
||||
if (isLineBreak(text[start])) {
|
||||
start++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Font font = glyphCache.lookupFont(text, start, limit, style);
|
||||
int next = font.canDisplayUpTo(text, start, limit);
|
||||
|
||||
@@ -1177,20 +1307,41 @@ public class StringCache {
|
||||
/* Creating a GlyphVector takes care of all language specific OpenType glyph substitutions and positionings */
|
||||
GlyphVector vector = glyphCache.layoutGlyphVector(font, text, start, limit, layoutFlags);
|
||||
|
||||
/*
|
||||
* Use the same GASP-aware FontRenderContext that was used to rasterize glyphs into the atlas.
|
||||
* Passing null here would use a default FRC that can produce different pixel bounds for hinted
|
||||
* glyphs, causing position mismatches and visible spacing gaps (especially on punctuation).
|
||||
*/
|
||||
java.awt.font.FontRenderContext frc = glyphCache.getFontRenderContext();
|
||||
|
||||
/*
|
||||
* Extract all needed information for each glyph from the GlyphVector so it won't be needed for actual rendering.
|
||||
* Note that initially, glyph.start holds the character index into the stripped text array. But after the entire
|
||||
* string is layed out, this field will be adjusted on every Glyph object to correctly index the original unstripped
|
||||
* string.
|
||||
*
|
||||
* Use getGlyphPosition() (logical font metrics) for horizontal placement and advance calculation.
|
||||
* Use getGlyphPixelBounds() only for the Y position and to compute the ink X offset from the logical X.
|
||||
* This separation ensures proper inter-glyph spacing (from logical metrics) while keeping correct
|
||||
* texture quad alignment (from ink bounds). Under GASP hinting, ink bounds can be tighter than
|
||||
* logical positions, causing glyphs to visually touch or overlap if used for spacing.
|
||||
*/
|
||||
Glyph glyph = null;
|
||||
int numGlyphs = vector.getNumGlyphs();
|
||||
int glyphPadding = glyphCache.getGlyphPadding();
|
||||
for (int index = 0; index < numGlyphs; index++) {
|
||||
Point position = vector.getGlyphPixelBounds(index, null, advance, 0).getLocation();
|
||||
int charIndex = start + vector.getGlyphCharIndex(index);
|
||||
if (charIndex >= start && charIndex < limit && isLineBreak(text[charIndex])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Compute horizontal advance for the previous glyph based on this glyph's position */
|
||||
Point2D logicalPos = vector.getGlyphPosition(index);
|
||||
int logicalX = advance + (int) logicalPos.getX();
|
||||
Rectangle inkBounds = vector.getGlyphPixelBounds(index, frc, advance, 0);
|
||||
|
||||
/* Compute horizontal advance for the previous glyph based on logical positions */
|
||||
if (glyph != null) {
|
||||
glyph.advance = position.x - glyph.x;
|
||||
glyph.advance = logicalX - glyph.x;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1198,10 +1349,11 @@ public class StringCache {
|
||||
* it will be corrected later to account for the color codes that have been stripped out.
|
||||
*/
|
||||
glyph = new Glyph();
|
||||
glyph.stringIndex = start + vector.getGlyphCharIndex(index);
|
||||
glyph.stringIndex = charIndex;
|
||||
glyph.texture = glyphCache.lookupGlyph(font, vector.getGlyphCode(index));
|
||||
glyph.x = position.x;
|
||||
glyph.y = position.y;
|
||||
glyph.x = logicalX;
|
||||
glyph.y = inkBounds.y - glyphPadding;
|
||||
glyph.inkOffsetX = (inkBounds.x - logicalX) - glyphPadding;
|
||||
glyphList.add(glyph);
|
||||
}
|
||||
|
||||
@@ -1215,5 +1367,3 @@ public class StringCache {
|
||||
return advance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,6 @@ public class AnimationController implements IAnimationHandler {
|
||||
File dir = getDir();
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir();
|
||||
return;
|
||||
}
|
||||
|
||||
categoryManager.loadCategories(dir);
|
||||
|
||||
@@ -67,6 +67,10 @@ public class CategoryManager {
|
||||
itemCategoryMap.remove(itemId);
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return baseDir != null;
|
||||
}
|
||||
|
||||
public File getItemDir(int itemId) {
|
||||
int catId = itemCategoryMap.getOrDefault(itemId, UNCATEGORIZED_ID);
|
||||
return getCategoryDir(catId);
|
||||
@@ -86,6 +90,7 @@ public class CategoryManager {
|
||||
// ========== Category CRUD ==========
|
||||
|
||||
public Category createCategory(String name) {
|
||||
if (baseDir == null) return null;
|
||||
while (hasCategoryName(name)) name += "_";
|
||||
lastUsedCatID++;
|
||||
Category cat = new Category(lastUsedCatID, name);
|
||||
@@ -96,7 +101,7 @@ public class CategoryManager {
|
||||
}
|
||||
|
||||
public void saveCategory(Category cat) {
|
||||
if (cat.id <= 0) return;
|
||||
if (cat.id <= 0 || baseDir == null) return;
|
||||
Category existing = categories.get(cat.id);
|
||||
if (existing != null && !existing.title.equals(cat.title)) {
|
||||
String newTitle = cat.title;
|
||||
@@ -116,6 +121,7 @@ public class CategoryManager {
|
||||
}
|
||||
|
||||
public boolean removeCategory(int catId, Set<Integer> allItemIds) {
|
||||
if (baseDir == null) return false;
|
||||
if (catId <= UNCATEGORIZED_ID) return false;
|
||||
Category cat = categories.get(catId);
|
||||
if (cat == null) return false;
|
||||
@@ -149,6 +155,7 @@ public class CategoryManager {
|
||||
}
|
||||
|
||||
public void moveItem(int itemId, String fileName, int destCatId) {
|
||||
if (baseDir == null) return;
|
||||
int oldCatId = itemCategoryMap.getOrDefault(itemId, UNCATEGORIZED_ID);
|
||||
if (oldCatId == destCatId) return;
|
||||
|
||||
|
||||
@@ -328,7 +328,6 @@ public class CustomEffectController implements ICustomEffectHandler {
|
||||
File dir = getDir();
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir();
|
||||
return;
|
||||
}
|
||||
|
||||
categoryManager.loadCategories(dir);
|
||||
|
||||
@@ -126,7 +126,6 @@ public class LinkedItemController implements ILinkedItemHandler {
|
||||
File dir = getDir();
|
||||
if (!dir.exists()) {
|
||||
dir.mkdir();
|
||||
return;
|
||||
}
|
||||
|
||||
categoryManager.loadCategories(dir);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package noppes.npcs.controllers;
|
||||
|
||||
import kamkeel.npcs.controllers.ProfileController;
|
||||
import kamkeel.npcs.controllers.data.profile.Profile;
|
||||
import noppes.npcs.client.ProfileClientConfig;
|
||||
import kamkeel.npcs.network.packets.data.AchievementPacket;
|
||||
import kamkeel.npcs.network.packets.data.ChatAlertPacket;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
@@ -131,14 +131,12 @@ public class PlayerQuestController {
|
||||
boolean finishedLocally = data.finishedQuests.containsKey(quest.id);
|
||||
boolean completedShared = false;
|
||||
boolean hasCooldownShared = false;
|
||||
if (ConfigMain.ProfilesEnabled && quest.profileOptions.enableOptions) {
|
||||
Profile profile = ProfileController.Instance.getProfile(player);
|
||||
if (profile != null && profile.sharedQuestTimestamps.containsKey(quest.id)) {
|
||||
if (quest.profileOptions.completeControl == EnumProfileSync.Shared)
|
||||
completedShared = true;
|
||||
if (quest.profileOptions.cooldownControl == EnumProfileSync.Shared)
|
||||
hasCooldownShared = true;
|
||||
}
|
||||
if (ProfileClientConfig.isProfilesEnabled() && quest.profileOptions.enableOptions
|
||||
&& ProfileClientConfig.hasSharedQuest(player, quest.id)) {
|
||||
if (quest.profileOptions.completeControl == EnumProfileSync.Shared)
|
||||
completedShared = true;
|
||||
if (quest.profileOptions.cooldownControl == EnumProfileSync.Shared)
|
||||
hasCooldownShared = true;
|
||||
}
|
||||
|
||||
boolean isFinished = finishedLocally || completedShared;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package noppes.npcs.controllers;
|
||||
|
||||
import kamkeel.npcs.controllers.ProfileController;
|
||||
import kamkeel.npcs.controllers.data.profile.Profile;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.config.ConfigMain;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ProfileConfigSync {
|
||||
public static NBTTagCompound writeToNBT(EntityPlayer player) {
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
if (!ConfigMain.ProfilesEnabled || ProfileController.Instance == null) {
|
||||
return compound;
|
||||
}
|
||||
|
||||
Profile profile = ProfileController.Instance.getProfile(player);
|
||||
if (profile == null) {
|
||||
return compound;
|
||||
}
|
||||
|
||||
NBTTagList questList = new NBTTagList();
|
||||
for (Map.Entry<Integer, Long> entry : profile.sharedQuestTimestamps.entrySet()) {
|
||||
NBTTagCompound questEntry = new NBTTagCompound();
|
||||
questEntry.setInteger("Quest", entry.getKey());
|
||||
questEntry.setLong("Date", entry.getValue());
|
||||
questList.appendTag(questEntry);
|
||||
}
|
||||
compound.setTag("SharedQuestTimestamps", questList);
|
||||
return compound;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public class ScriptContainer implements IScriptUnit {
|
||||
private static Method luaCall;
|
||||
private CompiledScript compScript = null;
|
||||
private final HashMap<String, ScriptObjectMirror> cachedFunctions = new HashMap<>();
|
||||
private long lastGlobalsVersion = -1;
|
||||
|
||||
/**
|
||||
* Persisted per-container language. Null means inherit from handler default.
|
||||
@@ -224,6 +225,7 @@ public class ScriptContainer implements IScriptUnit {
|
||||
if (ScriptController.Instance.lastLoaded > this.lastCreated) {
|
||||
this.lastCreated = ScriptController.Instance.lastLoaded;
|
||||
evaluated = false;
|
||||
lastGlobalsVersion = -1;
|
||||
}
|
||||
|
||||
synchronized (lock) {
|
||||
@@ -236,18 +238,24 @@ public class ScriptContainer implements IScriptUnit {
|
||||
engine.getContext().setWriter(pw);
|
||||
engine.getContext().setErrorWriter(pw);
|
||||
|
||||
// Populate bindings with API objects
|
||||
// Populate bindings with API objects (skip if globals unchanged)
|
||||
long currentGlobalsVersion = NpcAPI.engineObjectsVersion;
|
||||
boolean globalsChanged = lastGlobalsVersion != currentGlobalsVersion;
|
||||
if (instanceBindings != null) {
|
||||
instanceBindings.put("API", NpcAPI.Instance());
|
||||
HashMap<String, Object> engineEntries = new HashMap<>(NpcAPI.engineObjects);
|
||||
for (Map.Entry<String, Object> objectEntry : engineEntries.entrySet()) {
|
||||
instanceBindings.put(objectEntry.getKey(), objectEntry.getValue());
|
||||
if (globalsChanged) {
|
||||
instanceBindings.put("API", NpcAPI.Instance());
|
||||
for (Map.Entry<String, Object> objectEntry : NpcAPI.engineObjects.entrySet()) {
|
||||
instanceBindings.put(objectEntry.getKey(), objectEntry.getValue());
|
||||
}
|
||||
lastGlobalsVersion = currentGlobalsVersion;
|
||||
}
|
||||
} else {
|
||||
engine.put("API", NpcAPI.Instance());
|
||||
HashMap<String, Object> engineEntries = new HashMap<>(NpcAPI.engineObjects);
|
||||
for (Map.Entry<String, Object> objectEntry : engineEntries.entrySet()) {
|
||||
engine.put(objectEntry.getKey(), objectEntry.getValue());
|
||||
if (globalsChanged) {
|
||||
engine.put("API", NpcAPI.Instance());
|
||||
for (Map.Entry<String, Object> objectEntry : NpcAPI.engineObjects.entrySet()) {
|
||||
engine.put(objectEntry.getKey(), objectEntry.getValue());
|
||||
}
|
||||
lastGlobalsVersion = currentGlobalsVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +336,11 @@ public class ScriptContainer implements IScriptUnit {
|
||||
return !this.getFullCode().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isUnknownFunction(String type) {
|
||||
return unknownFunctions.contains(type);
|
||||
}
|
||||
|
||||
public void setEngine(String scriptLanguage) {
|
||||
if (!Objects.equals(scriptLanguage, this.currentScriptLanguage)) {
|
||||
// Instance scopes share the template's engine — never create a new one
|
||||
|
||||
@@ -45,6 +45,7 @@ public class DataScript implements IScriptHandlerPacket {
|
||||
public String scriptLanguage = "ECMAScript";
|
||||
private EntityNPCInterface npc;
|
||||
public boolean enabled = false;
|
||||
private long lastGlobalsVersion = -1;
|
||||
|
||||
public ICustomNpc dummyNpc;
|
||||
public IWorld dummyWorld;
|
||||
@@ -271,9 +272,13 @@ public class DataScript implements IScriptHandlerPacket {
|
||||
engine.put(entry.getKey(), entry.getValue().value);
|
||||
}
|
||||
|
||||
engine.put("API", NpcAPI.Instance());
|
||||
for (Map.Entry<String, Object> engineObjects : NpcAPI.engineObjects.entrySet()) {
|
||||
engine.put(engineObjects.getKey(), engineObjects.getValue());
|
||||
long currentGlobalsVersion = NpcAPI.engineObjectsVersion;
|
||||
if (lastGlobalsVersion != currentGlobalsVersion) {
|
||||
engine.put("API", NpcAPI.Instance());
|
||||
for (Map.Entry<String, Object> engineObjects : NpcAPI.engineObjects.entrySet()) {
|
||||
engine.put(engineObjects.getKey(), engineObjects.getValue());
|
||||
}
|
||||
lastGlobalsVersion = currentGlobalsVersion;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.io.IOException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class ForgeDataScript extends MultiScriptHandler {
|
||||
@@ -32,6 +33,7 @@ public class ForgeDataScript extends MultiScriptHandler {
|
||||
private static List<String> cachedHooks;
|
||||
|
||||
private long lastForgeUpdate = -1L;
|
||||
private final HashSet<String> globalUnknownEvents = new HashSet<>();
|
||||
|
||||
public ForgeDataScript() {
|
||||
}
|
||||
@@ -59,6 +61,7 @@ public class ForgeDataScript extends MultiScriptHandler {
|
||||
protected void reInitScripts() {
|
||||
lastInited = ScriptController.Instance.lastLoaded;
|
||||
lastForgeUpdate = ScriptController.Instance.lastForgeUpdate;
|
||||
globalUnknownEvents.clear();
|
||||
|
||||
for (IScriptUnit script : this.scripts) {
|
||||
if (script instanceof ScriptContainer)
|
||||
@@ -79,10 +82,25 @@ public class ForgeDataScript extends MultiScriptHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Skip events that no script tab handles
|
||||
if (globalUnknownEvents.contains(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean anyHandled = false;
|
||||
for (IScriptUnit script : this.scripts) {
|
||||
if (script == null || script.hasErrored() || !script.hasCode())
|
||||
continue;
|
||||
boolean wasUnknown = script.isUnknownFunction(type);
|
||||
script.run(type, event);
|
||||
if (!wasUnknown || !script.isUnknownFunction(type)) {
|
||||
anyHandled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no script tab handled this event type, cache it globally
|
||||
if (!anyHandled) {
|
||||
globalUnknownEvents.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,13 @@ public interface IScriptUnit {
|
||||
*/
|
||||
IScriptUnit createInstanceScope(IScriptHandler instanceHandler);
|
||||
|
||||
/**
|
||||
* Check if the given function name is known to be absent from this script unit.
|
||||
*/
|
||||
default boolean isUnknownFunction(String type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== EXECUTION ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package noppes.npcs.controllers.data;
|
||||
|
||||
import kamkeel.npcs.controllers.ProfileController;
|
||||
import kamkeel.npcs.controllers.data.profile.Profile;
|
||||
import noppes.npcs.client.ProfileClientConfig;
|
||||
import kamkeel.npcs.network.packets.data.QuestCompletionPacket;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
@@ -22,7 +21,6 @@ import noppes.npcs.api.handler.data.IQuestCategory;
|
||||
import noppes.npcs.api.handler.data.IQuestInterface;
|
||||
import noppes.npcs.api.handler.data.IQuestObjective;
|
||||
import noppes.npcs.api.handler.data.ISlot;
|
||||
import noppes.npcs.config.ConfigMain;
|
||||
import noppes.npcs.constants.EnumProfileSync;
|
||||
import noppes.npcs.constants.EnumQuestCompletion;
|
||||
import noppes.npcs.constants.EnumQuestRepeat;
|
||||
@@ -176,13 +174,13 @@ public class Quest implements ICompatibilty, IQuest {
|
||||
}
|
||||
|
||||
public long getTimeUntilRepeat(EntityPlayer player) {
|
||||
if (ConfigMain.ProfilesEnabled && profileOptions.enableOptions && profileOptions.cooldownControl == EnumProfileSync.Shared) {
|
||||
Profile profile = ProfileController.Instance.getProfile(player);
|
||||
if (profile != null) {
|
||||
Long questTime = profile.sharedQuestTimestamps.get(this.id);
|
||||
if (questTime == null) return 0L;
|
||||
if (ProfileClientConfig.isProfilesEnabled() && profileOptions.enableOptions
|
||||
&& profileOptions.cooldownControl == EnumProfileSync.Shared) {
|
||||
Long questTime = ProfileClientConfig.getSharedQuestTimestamp(player, this.id);
|
||||
if (questTime != null) {
|
||||
return calculateRemainingCooldown(player, questTime);
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
IPlayerQuestData questData = PlayerData.get(player).getQuestData();
|
||||
return getTimeUntilRepeatQuestData(player, questData);
|
||||
|
||||
@@ -160,6 +160,7 @@ public class NpcAPI extends AbstractNpcAPI {
|
||||
private static final Map<Integer, ScriptWorld> worldCache = new LRUHashMap<>(10);
|
||||
private static final CacheHashMap<ItemStack, CacheHashMap.CachedObject<ScriptItemStack>> scriptItemCache = new CacheHashMap<>(60 * 1000);
|
||||
public static final HashMap<String, Object> engineObjects = new HashMap<>();
|
||||
public static volatile long engineObjectsVersion = 0;
|
||||
public static final EventBus EVENT_BUS = new EventBus();
|
||||
private static AbstractNpcAPI instance = null;
|
||||
|
||||
@@ -255,10 +256,12 @@ public class NpcAPI extends AbstractNpcAPI {
|
||||
|
||||
public void addGlobalObject(String key, Object obj) {
|
||||
NpcAPI.engineObjects.put(key, obj);
|
||||
NpcAPI.engineObjectsVersion++;
|
||||
}
|
||||
|
||||
public void removeGlobalObject(String key) {
|
||||
NpcAPI.engineObjects.remove(key);
|
||||
NpcAPI.engineObjectsVersion++;
|
||||
}
|
||||
|
||||
public boolean hasGlobalObject(String key) {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"background": {
|
||||
"backgroundColor": "#FF000000",
|
||||
"gutterColor": "#FF000000",
|
||||
"gutterSeparatorColor": "#FF3C3F41",
|
||||
"lineNumberColor": "#FF606366",
|
||||
"lineNumberActiveColor": "#FFB9C7D6",
|
||||
"scrollbarColor": "#FFE0E0E0",
|
||||
"borderColor": "#FFA0A0A0",
|
||||
"caretColor": "#FFFFFFFF"
|
||||
|
||||
},
|
||||
"COMMENT": {
|
||||
"color": "#FF777777",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"STRING": {
|
||||
"color": "#FF16c57d",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"JSDOC_TAG": {
|
||||
"color": "#FF8E8070",
|
||||
"bold": true,
|
||||
"italic": false
|
||||
},
|
||||
"JSDOC_TYPE": {
|
||||
"color": "#FF9A8C7C",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"UNUSED_IMPORT": {
|
||||
"color": "#FF666666",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"KEYWORD": {
|
||||
"color": "#FFFF5555",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"INTERFACE_DECL": {
|
||||
"color": "#FF80F2F6",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"ENUM_DECL": {
|
||||
"color": "#FFFF55FF",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"ENUM_CONSTANT": {
|
||||
"color": "#FF55FFFF",
|
||||
"bold": true,
|
||||
"italic": true
|
||||
},
|
||||
"CLASS_DECL": {
|
||||
"color": "#FF00AAAA",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"IMPORTED_CLASS": {
|
||||
"color": "#FF00AAAA",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"GENERIC_TYPE_PARAM": {
|
||||
"color": "#FF00FA9A",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"TYPE_DECL": {
|
||||
"color": "#FF00AAAA",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"METHOD_DECL": {
|
||||
"color": "#FF00AA00",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"METHOD_CALL": {
|
||||
"color": "#FF55FF55",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"UNDEFINED_VAR": {
|
||||
"color": "#FFAA0000",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"PARAMETER": {
|
||||
"color": "#FF5555FF",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"GLOBAL_FIELD": {
|
||||
"color": "#FF55FFFF",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"LOCAL_FIELD": {
|
||||
"color": "#FFFFFF55",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"STATIC_FINAL_FIELD": {
|
||||
"color": "#FF8DDAF8",
|
||||
"bold": true,
|
||||
"italic": true
|
||||
},
|
||||
"LITERAL": {
|
||||
"color": "#FF79C0FF",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"VARIABLE": {
|
||||
"color": "#FFFFFFFF",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
},
|
||||
"DEFAULT": {
|
||||
"color": "#FFFFFFFF",
|
||||
"bold": false,
|
||||
"italic": false
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in new issue
Block a user