Faster loading XP Widgets, file loading heleprs and XP widget resetting

This commit is contained in:
downthecrop 2025-08-19 23:56:25 -07:00
parent 7f09263209
commit 7cc56d0e53
5 changed files with 255 additions and 99 deletions

View file

@ -3,6 +3,8 @@ package KondoKit
import rt4.GameShell import rt4.GameShell
import java.awt.* import java.awt.*
import java.awt.event.MouseListener import java.awt.event.MouseListener
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
@ -11,6 +13,16 @@ import java.util.Timer
import javax.swing.* import javax.swing.*
object Helpers { object Helpers {
// Convenience helper for loading resources relative to the KondoKit plugin class
fun openResource(path: String) = plugin::class.java.getResourceAsStream(path)
// Read a bundled resource as text using the given charset (UTF-8 by default)
fun readResourceText(path: String, charset: Charset = StandardCharsets.UTF_8): String? {
val stream = openResource(path) ?: return null
stream.use { s ->
return s.reader(charset).use { it.readText() }
}
}
fun convertValue(type: Class<*>, genericType: Type?, value: String): Any { fun convertValue(type: Class<*>, genericType: Type?, value: String): Any {
return when { return when {
@ -233,4 +245,4 @@ object Helpers {
else -> Color(128, 128, 128) // Default grey for unhandled skill IDs else -> Color(128, 128, 128) // Default grey for unhandled skill IDs
} }
} }
} }

View file

@ -79,23 +79,21 @@ object LootTrackerView {
} else { } else {
try { try {
println("LootTracker: Loading Local GE Prices") println("LootTracker: Loading Local GE Prices")
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/item_configs.json"), StandardCharsets.UTF_8)) Helpers.readResourceText("res/item_configs.json")?.let { json ->
.useLines { lines -> val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val json = lines.joinToString("\n") val gePrices = mutableMapOf<String, String>()
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
for (item in items) { for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",") val pairs = item.removeSurrounding("{", "}").split(",")
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"') val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"') val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (id != null && grandExchangePrice != null) { if (id != null && grandExchangePrice != null) {
gePrices[id] = grandExchangePrice gePrices[id] = grandExchangePrice
}
} }
gePrices
} }
gePrices
} ?: emptyMap()
} catch (e: Exception) { } catch (e: Exception) {
emptyMap() emptyMap()
} }

View file

@ -70,6 +70,8 @@ object ReflectiveEditorView {
mainPanel = JPanel(cardLayout) mainPanel = JPanel(cardLayout)
mainPanel.background = VIEW_BACKGROUND_COLOR mainPanel.background = VIEW_BACKGROUND_COLOR
mainPanel.border = BorderFactory.createEmptyBorder(0, 0, 0, 0) mainPanel.border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
// Help minimize flicker during dynamic swaps
mainPanel.isDoubleBuffered = true
// Create the plugin list view // Create the plugin list view
val pluginListView = createPluginListView() val pluginListView = createPluginListView()
@ -636,25 +638,35 @@ object ReflectiveEditorView {
} }
fun addPlugins(reflectiveEditorView: JPanel) { fun addPlugins(reflectiveEditorView: JPanel) {
// Refresh the plugin list by recreating the plugin list view // Ensure we run on the EDT; if not, reschedule and return
if (!SwingUtilities.isEventDispatchThread()) {
// Remove the existing plugin list view SwingUtilities.invokeLater { addPlugins(reflectiveEditorView) }
val existingListView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW } return
if (existingListView != null) { }
mainPanel.remove(existingListView)
// Batch updates to avoid intermediate repaints/flicker
mainPanel.ignoreRepaint = true
try {
// Remove the existing plugin list view if present
val existingListView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW }
if (existingListView != null) {
mainPanel.remove(existingListView)
}
// Create a new plugin list view off-EDT (already on EDT here) and add it
val pluginListView = createPluginListView()
pluginListView.name = PLUGIN_LIST_VIEW
mainPanel.add(pluginListView, PLUGIN_LIST_VIEW)
// Switch card after the new component is in place
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
// Revalidate/repaint once at the end
mainPanel.revalidate()
mainPanel.repaint()
} finally {
mainPanel.ignoreRepaint = false
} }
// Create a new plugin list view
val pluginListView = createPluginListView()
pluginListView.name = PLUGIN_LIST_VIEW
mainPanel.add(pluginListView, PLUGIN_LIST_VIEW)
// Revalidate and repaint the main panel
mainPanel.revalidate()
mainPanel.repaint()
// Show the plugin list view
cardLayout.show(mainPanel, PLUGIN_LIST_VIEW)
} }
var customToolTipWindow: JWindow? = null var customToolTipWindow: JWindow? = null
@ -753,4 +765,4 @@ object ReflectiveEditorView {
g2.fillRoundRect(x, y, width - 1, height - 1, radius, radius) g2.fillRoundRect(x, y, width - 1, height - 1, radius, radius)
} }
} }
} }

View file

@ -34,27 +34,25 @@ object XPTrackerView {
val initialXP: MutableMap<Int, Int> = HashMap() val initialXP: MutableMap<Int, Int> = HashMap()
var xpTrackerView: JPanel? = null var xpTrackerView: JPanel? = null
const val VIEW_NAME = "XP_TRACKER_VIEW" const val VIEW_NAME = "XP_TRACKER_VIEW"
private val skillIconCache: MutableMap<Int, java.awt.image.BufferedImage> = HashMap()
val npcHitpointsMap: Map<Int, Int> = try { val npcHitpointsMap: Map<Int, Int> = try {
BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/npc_hitpoints_map.json"), StandardCharsets.UTF_8)) val json = Helpers.readResourceText("res/npc_hitpoints_map.json") ?: "{}"
.useLines { lines -> val pairs = json.trim().removeSurrounding("{", "}").split(",")
val json = lines.joinToString("\n") val map = mutableMapOf<Int, Int>()
val pairs = json.trim().removeSurrounding("{", "}").split(",")
val map = mutableMapOf<Int, Int>()
for (pair in pairs) { for (pair in pairs) {
val keyValue = pair.split(":") val keyValue = pair.split(":")
val id = keyValue[0].trim().trim('\"').toIntOrNull() val id = keyValue[0].trim().trim('\"').toIntOrNull()
val hitpoints = keyValue[1].trim() val hitpoints = keyValue[1].trim()
if (id != null && hitpoints.isNotEmpty()) { if (id != null && hitpoints.isNotEmpty()) {
map[id] = hitpoints.toIntOrNull() ?: 0 map[id] = hitpoints.toIntOrNull() ?: 0
}
}
map
} }
}
map
} catch (e: Exception) { } catch (e: Exception) {
println("XPTracker Error parsing NPC HP: ${e.message}") println("XPTracker Error parsing NPC HP: ${e.message}")
emptyMap() emptyMap()
@ -267,6 +265,18 @@ object XPTrackerView {
widgetViewPanel.add(Box.createVerticalStrut(5)) widgetViewPanel.add(Box.createVerticalStrut(5))
xpTrackerView = widgetViewPanel xpTrackerView = widgetViewPanel
// Preload skill icons to avoid first-drop lag
try {
for (i in 0 until 24) {
if (!skillIconCache.containsKey(i)) {
val img = getBufferedImageFromSprite(API.GetSprite(getSpriteId(i)))
skillIconCache[i] = img
}
}
} catch (_: Exception) {
// Ignore preload errors; fallback at use time
}
} }
@ -293,6 +303,61 @@ object XPTrackerView {
return popupMenu return popupMenu
} }
fun removeXPWidgetMenu(toRemove: Container, skillId: Int): JPopupMenu {
val popupMenu = JPopupMenu()
val rFont = Font("RuneScape Small", Font.TRUETYPE_FONT, 16)
popupMenu.background = POPUP_BACKGROUND
val resetItem = JMenuItem("Reset").apply {
font = rFont
background = POPUP_BACKGROUND
foreground = POPUP_FOREGROUND
}
popupMenu.add(resetItem)
val removeItem = JMenuItem("Remove").apply {
font = rFont
background = POPUP_BACKGROUND
foreground = POPUP_FOREGROUND
}
popupMenu.add(removeItem)
resetItem.addActionListener {
xpWidgets[skillId]?.let { widget ->
// Baseline at current XP and clear per-widget counters
initialXP[skillId] = widget.previousXp
widget.totalXpGained = 0
widget.startTime = System.currentTimeMillis()
// Recompute labels/progress for current XP without adding totals
updateWidget(widget, widget.previousXp)
}
}
removeItem.addActionListener {
// Reset the per-skill baseline to the current XP so next widget starts fresh
xpWidgets[skillId]?.let { widget ->
initialXP[skillId] = widget.previousXp
}
// Remove widget container and following spacer if present
xpTrackerView?.let { parent ->
val components = parent.components
val toRemoveIndex = components.indexOf(toRemove)
if (toRemoveIndex >= 0 && toRemoveIndex < components.size - 1) {
val nextComponent = components[toRemoveIndex + 1]
if (nextComponent is Box.Filler) {
parent.remove(nextComponent)
}
}
parent.remove(toRemove)
xpWidgets.remove(skillId)
parent.revalidate()
if (focusedView == VIEW_NAME) parent.repaint()
}
}
return popupMenu
}
fun createXPWidget(skillId: Int, previousXp: Int): XPWidget { fun createXPWidget(skillId: Int, previousXp: Int): XPWidget {
val widgetPanel = Panel().apply { val widgetPanel = Panel().apply {
@ -303,7 +368,12 @@ object XPTrackerView {
minimumSize = WIDGET_SIZE minimumSize = WIDGET_SIZE
} }
val bufferedImageSprite = getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId))) val bufferedImageSprite = skillIconCache[skillId]
?: run {
val img = getBufferedImageFromSprite(API.GetSprite(getSpriteId(skillId)))
skillIconCache[skillId] = img
img
}
val imageContainer = Panel(FlowLayout()).apply { val imageContainer = Panel(FlowLayout()).apply {
background = WIDGET_COLOR background = WIDGET_COLOR
preferredSize = IMAGE_SIZE preferredSize = IMAGE_SIZE
@ -441,4 +511,4 @@ data class XPWidget(
var totalXpGained: Int = 0, var totalXpGained: Int = 0,
var startTime: Long = System.currentTimeMillis(), var startTime: Long = System.currentTimeMillis(),
var previousXp: Int = 0 var previousXp: Int = 0
) )

View file

@ -164,11 +164,15 @@ class plugin : Plugin() {
override fun OnPluginsReloaded(): Boolean { override fun OnPluginsReloaded(): Boolean {
if (!initialized) return true if (!initialized) return true
updateDisplaySettings() // Ensure Swing updates happen on the EDT to avoid flicker
frame.remove(rightPanelWrapper) SwingUtilities.invokeLater {
frame.layout = BorderLayout() updateDisplaySettings()
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) } frame.remove(rightPanelWrapper)
frame.revalidate() frame.layout = BorderLayout()
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
frame.revalidate()
frame.repaint()
}
pluginsReloaded = true pluginsReloaded = true
reloadInterfaces = true reloadInterfaces = true
return true return true
@ -179,25 +183,47 @@ class plugin : Plugin() {
initialXP[skillId] = xp initialXP[skillId] = xp
return return
} }
var xpWidget = xpWidgets[skillId] val previousXpSnapshot = initialXP[skillId] ?: xp
if (xpWidget != null) { if (xp == initialXP[skillId]) return
updateWidget(xpWidget, xp)
} else {
val previousXp = initialXP[skillId] ?: xp
if (xp == initialXP[skillId]) return
xpWidget = createXPWidget(skillId, previousXp) val ensureOnEdt = Runnable {
xpWidgets[skillId] = xpWidget var xpWidget = xpWidgets[skillId]
if (xpWidget != null) {
updateWidget(xpWidget, xp)
} else {
xpWidget = createXPWidget(skillId, previousXpSnapshot)
xpWidgets[skillId] = xpWidget
xpTrackerView?.add(wrappedWidget(xpWidget.container)) val wrapped = wrappedWidget(xpWidget.container)
xpTrackerView?.add(Box.createVerticalStrut(5)) // Attach per-widget remove menu
val popupMenu = XPTrackerView.removeXPWidgetMenu(wrapped, skillId)
val rightClickListener = object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y)
}
override fun mouseReleased(e: MouseEvent) {
if (e.isPopupTrigger) popupMenu.show(e.component, e.x, e.y)
}
}
Helpers.addMouseListenerToAll(wrapped, rightClickListener)
wrapped.addMouseListener(rightClickListener)
if(focusedView == XPTrackerView.VIEW_NAME) { xpTrackerView?.add(wrapped)
xpTrackerView?.revalidate() xpTrackerView?.add(Box.createVerticalStrut(5))
xpTrackerView?.repaint()
if(focusedView == XPTrackerView.VIEW_NAME) {
xpTrackerView?.revalidate()
xpTrackerView?.repaint()
}
updateWidget(xpWidget, xp)
} }
}
updateWidget(xpWidget, xp) if (SwingUtilities.isEventDispatchThread()) {
ensureOnEdt.run()
} else {
SwingUtilities.invokeLater(ensureOnEdt)
} }
} }
@ -208,7 +234,10 @@ class plugin : Plugin() {
} }
if (pluginsReloaded) { if (pluginsReloaded) {
reflectiveEditorView?.let { addPlugins(it) } // Rebuild the reflective editor UI on the EDT and in one batch
SwingUtilities.invokeLater {
reflectiveEditorView?.let { addPlugins(it) }
}
pluginsReloaded = false pluginsReloaded = false
} }
@ -481,15 +510,24 @@ class plugin : Plugin() {
verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER
} }
frame.layout = BorderLayout() val desiredView = if (launchMinimized) HIDDEN_VIEW else XPTrackerView.VIEW_NAME
rightPanelWrapper?.let { // Commit layout synchronously on the EDT to avoid initial misplacement
frame.add(it, BorderLayout.EAST) val commit = Runnable {
frame.layout = BorderLayout()
rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) }
setActiveView(desiredView)
frame.validate()
frame.repaint()
} }
if (SwingUtilities.isEventDispatchThread()) {
if(launchMinimized){ commit.run()
setActiveView(HIDDEN_VIEW)
} else { } else {
setActiveView(XPTrackerView.VIEW_NAME) try {
javax.swing.SwingUtilities.invokeAndWait(commit)
} catch (e: Exception) {
// Fallback to async if invokeAndWait fails for any reason
SwingUtilities.invokeLater(commit)
}
} }
initialized = true initialized = true
pluginsReloaded = true pluginsReloaded = true
@ -497,29 +535,55 @@ class plugin : Plugin() {
} }
private fun setActiveView(viewName: String) { private fun setActiveView(viewName: String) {
// Handle the visibility of the main content panel val runUpdate: () -> Unit = {
if (viewName == HIDDEN_VIEW) { // Track visibility change to decide if we need to resize/reload interfaces
mainContentPanel.isVisible = false val wasVisible = mainContentPanel.isVisible
} else {
if (!mainContentPanel.isVisible) { // Handle the visibility of the main content panel and card switch
mainContentPanel.isVisible = true if (viewName == HIDDEN_VIEW) {
mainContentPanel.isVisible = false
} else {
if (!mainContentPanel.isVisible) {
mainContentPanel.isVisible = true
}
cardLayout.show(mainContentPanel, viewName)
} }
cardLayout.show(mainContentPanel, viewName)
val visibilityChanged = wasVisible != mainContentPanel.isVisible
// Batch painting to avoid intermediate repaints
rightPanelWrapper?.ignoreRepaint = true
try {
if (visibilityChanged) {
// Only touch layout and client interfaces if width actually changes
updateDisplaySettings()
reloadInterfaces = true
rightPanelWrapper?.revalidate()
frame?.validate()
} else {
// Just a card switch; avoid full frame revalidate
mainContentPanel.revalidate()
}
} finally {
rightPanelWrapper?.ignoreRepaint = false
}
// Targeted repaint for snappy feedback
if (visibilityChanged) {
rightPanelWrapper?.repaint()
frame?.repaint()
} else {
mainContentPanel.repaint()
}
focusedView = viewName
} }
reloadInterfaces = true if (SwingUtilities.isEventDispatchThread()) {
updateDisplaySettings() runUpdate()
} else {
// Revalidate and repaint necessary panels SwingUtilities.invokeLater { runUpdate() }
mainContentPanel.revalidate() }
rightPanelWrapper?.revalidate()
frame?.revalidate()
mainContentPanel.repaint()
rightPanelWrapper?.repaint()
frame?.repaint()
focusedView = viewName
} }
private fun createNavButton(spriteId: Int, viewName: String): JPanel { private fun createNavButton(spriteId: Int, viewName: String): JPanel {
@ -666,7 +730,7 @@ class plugin : Plugin() {
} }
private fun loadFont(): Font? { private fun loadFont(): Font? {
val fontStream = plugin::class.java.getResourceAsStream("res/runescape_small.ttf") val fontStream = Helpers.openResource("res/runescape_small.ttf")
return if (fontStream != null) { return if (fontStream != null) {
try { try {
val font = Font.createFont(Font.TRUETYPE_FONT, fontStream) val font = Font.createFont(Font.TRUETYPE_FONT, fontStream)