From 7cc56d0e5369b376b074b72eb7f45aa43b702d53 Mon Sep 17 00:00:00 2001 From: downthecrop Date: Tue, 19 Aug 2025 23:56:25 -0700 Subject: [PATCH] Faster loading XP Widgets, file loading heleprs and XP widget resetting --- .../src/main/kotlin/KondoKit/Helpers.kt | 14 +- .../main/kotlin/KondoKit/LootTrackerView.kt | 26 ++- .../kotlin/KondoKit/ReflectiveEditorView.kt | 50 +++--- .../src/main/kotlin/KondoKit/XPTrackerView.kt | 104 ++++++++++-- .../src/main/kotlin/KondoKit/plugin.kt | 160 ++++++++++++------ 5 files changed, 255 insertions(+), 99 deletions(-) diff --git a/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt b/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt index 4f6e3c4..9a89e33 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/Helpers.kt @@ -3,6 +3,8 @@ package KondoKit import rt4.GameShell import java.awt.* import java.awt.event.MouseListener +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets import java.lang.reflect.Field import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -11,6 +13,16 @@ import java.util.Timer import javax.swing.* 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 { return when { @@ -233,4 +245,4 @@ object Helpers { else -> Color(128, 128, 128) // Default grey for unhandled skill IDs } } -} \ No newline at end of file +} diff --git a/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt b/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt index cfdb4a2..fcf0285 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/LootTrackerView.kt @@ -79,23 +79,21 @@ object LootTrackerView { } else { try { println("LootTracker: Loading Local GE Prices") - BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/item_configs.json"), StandardCharsets.UTF_8)) - .useLines { lines -> - val json = lines.joinToString("\n") - val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" } - val gePrices = mutableMapOf() + Helpers.readResourceText("res/item_configs.json")?.let { json -> + val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" } + val gePrices = mutableMapOf() - for (item in items) { - val pairs = item.removeSurrounding("{", "}").split(",") - 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('\"') - if (id != null && grandExchangePrice != null) { - gePrices[id] = grandExchangePrice - } + for (item in items) { + val pairs = item.removeSurrounding("{", "}").split(",") + 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('\"') + if (id != null && grandExchangePrice != null) { + gePrices[id] = grandExchangePrice } - - gePrices } + + gePrices + } ?: emptyMap() } catch (e: Exception) { emptyMap() } diff --git a/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt b/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt index 61f7a24..cb5e5b2 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/ReflectiveEditorView.kt @@ -70,6 +70,8 @@ object ReflectiveEditorView { mainPanel = JPanel(cardLayout) mainPanel.background = VIEW_BACKGROUND_COLOR mainPanel.border = BorderFactory.createEmptyBorder(0, 0, 0, 0) + // Help minimize flicker during dynamic swaps + mainPanel.isDoubleBuffered = true // Create the plugin list view val pluginListView = createPluginListView() @@ -636,25 +638,35 @@ object ReflectiveEditorView { } fun addPlugins(reflectiveEditorView: JPanel) { - // Refresh the plugin list by recreating the plugin list view - - // Remove the existing plugin list view - val existingListView = mainPanel.components.find { it.name == PLUGIN_LIST_VIEW } - if (existingListView != null) { - mainPanel.remove(existingListView) + // Ensure we run on the EDT; if not, reschedule and return + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater { addPlugins(reflectiveEditorView) } + return + } + + // 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 @@ -753,4 +765,4 @@ object ReflectiveEditorView { g2.fillRoundRect(x, y, width - 1, height - 1, radius, radius) } } -} \ No newline at end of file +} diff --git a/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt b/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt index b975194..4924b91 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/XPTrackerView.kt @@ -34,27 +34,25 @@ object XPTrackerView { val initialXP: MutableMap = HashMap() var xpTrackerView: JPanel? = null const val VIEW_NAME = "XP_TRACKER_VIEW" + private val skillIconCache: MutableMap = HashMap() val npcHitpointsMap: Map = try { - BufferedReader(InputStreamReader(plugin::class.java.getResourceAsStream("res/npc_hitpoints_map.json"), StandardCharsets.UTF_8)) - .useLines { lines -> - val json = lines.joinToString("\n") - val pairs = json.trim().removeSurrounding("{", "}").split(",") - val map = mutableMapOf() + val json = Helpers.readResourceText("res/npc_hitpoints_map.json") ?: "{}" + val pairs = json.trim().removeSurrounding("{", "}").split(",") + val map = mutableMapOf() - for (pair in pairs) { - val keyValue = pair.split(":") - val id = keyValue[0].trim().trim('\"').toIntOrNull() - val hitpoints = keyValue[1].trim() + for (pair in pairs) { + val keyValue = pair.split(":") + val id = keyValue[0].trim().trim('\"').toIntOrNull() + val hitpoints = keyValue[1].trim() - if (id != null && hitpoints.isNotEmpty()) { - map[id] = hitpoints.toIntOrNull() ?: 0 - } - } - - map + if (id != null && hitpoints.isNotEmpty()) { + map[id] = hitpoints.toIntOrNull() ?: 0 } + } + + map } catch (e: Exception) { println("XPTracker Error parsing NPC HP: ${e.message}") emptyMap() @@ -267,6 +265,18 @@ object XPTrackerView { widgetViewPanel.add(Box.createVerticalStrut(5)) 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 } + 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 { val widgetPanel = Panel().apply { @@ -303,7 +368,12 @@ object XPTrackerView { 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 { background = WIDGET_COLOR preferredSize = IMAGE_SIZE @@ -441,4 +511,4 @@ data class XPWidget( var totalXpGained: Int = 0, var startTime: Long = System.currentTimeMillis(), var previousXp: Int = 0 -) \ No newline at end of file +) diff --git a/plugin-playground/src/main/kotlin/KondoKit/plugin.kt b/plugin-playground/src/main/kotlin/KondoKit/plugin.kt index fcf4666..ab2b620 100644 --- a/plugin-playground/src/main/kotlin/KondoKit/plugin.kt +++ b/plugin-playground/src/main/kotlin/KondoKit/plugin.kt @@ -164,11 +164,15 @@ class plugin : Plugin() { override fun OnPluginsReloaded(): Boolean { if (!initialized) return true - updateDisplaySettings() - frame.remove(rightPanelWrapper) - frame.layout = BorderLayout() - rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) } - frame.revalidate() + // Ensure Swing updates happen on the EDT to avoid flicker + SwingUtilities.invokeLater { + updateDisplaySettings() + frame.remove(rightPanelWrapper) + frame.layout = BorderLayout() + rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) } + frame.revalidate() + frame.repaint() + } pluginsReloaded = true reloadInterfaces = true return true @@ -179,25 +183,47 @@ class plugin : Plugin() { initialXP[skillId] = xp return } - var xpWidget = xpWidgets[skillId] - if (xpWidget != null) { - updateWidget(xpWidget, xp) - } else { - val previousXp = initialXP[skillId] ?: xp - if (xp == initialXP[skillId]) return + val previousXpSnapshot = initialXP[skillId] ?: xp + if (xp == initialXP[skillId]) return - xpWidget = createXPWidget(skillId, previousXp) - xpWidgets[skillId] = xpWidget + val ensureOnEdt = Runnable { + var xpWidget = xpWidgets[skillId] + if (xpWidget != null) { + updateWidget(xpWidget, xp) + } else { + xpWidget = createXPWidget(skillId, previousXpSnapshot) + xpWidgets[skillId] = xpWidget - xpTrackerView?.add(wrappedWidget(xpWidget.container)) - xpTrackerView?.add(Box.createVerticalStrut(5)) + val wrapped = wrappedWidget(xpWidget.container) + // 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?.revalidate() - xpTrackerView?.repaint() + xpTrackerView?.add(wrapped) + xpTrackerView?.add(Box.createVerticalStrut(5)) + + 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) { - reflectiveEditorView?.let { addPlugins(it) } + // Rebuild the reflective editor UI on the EDT and in one batch + SwingUtilities.invokeLater { + reflectiveEditorView?.let { addPlugins(it) } + } pluginsReloaded = false } @@ -481,15 +510,24 @@ class plugin : Plugin() { verticalScrollBarPolicy = JScrollPane.VERTICAL_SCROLLBAR_NEVER } - frame.layout = BorderLayout() - rightPanelWrapper?.let { - frame.add(it, BorderLayout.EAST) + val desiredView = if (launchMinimized) HIDDEN_VIEW else XPTrackerView.VIEW_NAME + // Commit layout synchronously on the EDT to avoid initial misplacement + val commit = Runnable { + frame.layout = BorderLayout() + rightPanelWrapper?.let { frame.add(it, BorderLayout.EAST) } + setActiveView(desiredView) + frame.validate() + frame.repaint() } - - if(launchMinimized){ - setActiveView(HIDDEN_VIEW) + if (SwingUtilities.isEventDispatchThread()) { + commit.run() } 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 pluginsReloaded = true @@ -497,29 +535,55 @@ class plugin : Plugin() { } private fun setActiveView(viewName: String) { - // Handle the visibility of the main content panel - if (viewName == HIDDEN_VIEW) { - mainContentPanel.isVisible = false - } else { - if (!mainContentPanel.isVisible) { - mainContentPanel.isVisible = true + val runUpdate: () -> Unit = { + // Track visibility change to decide if we need to resize/reload interfaces + val wasVisible = mainContentPanel.isVisible + + // Handle the visibility of the main content panel and card switch + 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 - updateDisplaySettings() - - // Revalidate and repaint necessary panels - mainContentPanel.revalidate() - rightPanelWrapper?.revalidate() - frame?.revalidate() - - mainContentPanel.repaint() - rightPanelWrapper?.repaint() - frame?.repaint() - - focusedView = viewName + if (SwingUtilities.isEventDispatchThread()) { + runUpdate() + } else { + SwingUtilities.invokeLater { runUpdate() } + } } private fun createNavButton(spriteId: Int, viewName: String): JPanel { @@ -666,7 +730,7 @@ class plugin : Plugin() { } 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) { try { val font = Font.createFont(Font.TRUETYPE_FONT, fontStream)